Как использовать лучшее из .NET в неуправляемом коде на C++

Источник: codingclub

Managed Extensions (управляемые расширения) для C++ позволяют свободно смешивать неуправляемый (native) и управляемый код даже в одном модуле. Ух ты, жизнь прекрасна! Однако компиляция с ключом /clr может иметь нежелательные последствия. Этот ключ навязывает многопоточный режим и отключает некоторые полезные проверки периода выполнения. Он влияет на DEBUG_NEW в MFC, и некоторые классы .NET Framework могут конфликтовать с вашим пространством имен. И что делать, если в устаревшем приложении используется компилятор, не поддерживающий /clr? Есть ли способ воспользоваться Framework без Managed Extensions? Да!

В этой статье я покажу, как обернуть Framework-классы так, чтобы их можно было использовать в любом приложении на C++/MFC без /clr. В данном случае я оберну классы регулярных выражений .NET Framework в DLL и реализую три MFC-программы, использующие их. RegexWrap.dll позволяет добавлять регулярные выражения в любые приложения на C++/MFC, а набор моих инструментов ManWrap дает вам возможность самостоятельно заключать в оболочки ваши любимые Framework-классы.

Простой вопрос

Все началось с простого вопроса от читателя Анирбана Гупаты (Anirban Gupata): существует ли библиотека регулярных выражений, которую можно задействовать в приложении на C++? Я ответил: "Конечно, даже несколько. Но в .NET уже есть класс Regex. Чем он вас не устраивает?" Регулярные выражения настолько полезны (см. врезку "Регулярные выражения"), что могут заставить даже самого упертого фанатика С++ позавидовать тем, кто работает с .NET Framework. Так что я написал маленькую программу RegexTest, демонстрирующую возможности Regex. На рис. 1 показан ее внешний вид. Вы вводите регулярное выражение и входную строку, щелкаете кнопку, и RegexTest отображает Matches, Groups и Captures. Вся работа выполняется в единственной функции FormatResults (рис. 2), форматирующей большую строку класса CString по щелчку OK. FormatResults - единственная функция в RegexTest, вызывающая Framework, так что ее легко выделить в свой модуль, компилируемый с ключом /clr.

Рис. 1. RegexTest

Если бы моя задача ограничивалась написанием RegexTest, все было бы закончено. Но это приложение заставило меня задуматься. Реальные приложения хранят свои объекты дольше, чем выполняется функция. Допустим, я хочу хранить регулярное выражение в оконном классе. Хорошая идея, но, к несчастью, нельзя хранить __gc-указатели в неуправляемой памяти:

class CMyWnd ... {
protected:
Regex* m_regex; // НЕТ!
};

Вместо этого нужно применять GCHandle или его шаблонную версию, gcroot:

class CMyWnd ... {
protected:
gcroot<Regex*> m_regex; // отлично!
};

GCHandle и gcroot отлично описаны в документации и других источниках [см. "Tips and Tricks to Bolster Your Managed C++ Code" Томаса Рестрепо (Tomas Restrepo) в "MSDN Magazine" за февраль 2002 г. по ссылке msdn.microsoft.com/msdnmag/issues/02/02/managedc], поэтому я ограничусь упоминанием о том, что gcroot использует шаблоны и перегрузку операторов C++, - это позволяет описателям вести себя подобно указателям. Их можно копировать, присваивать и преобразовывать; оператор разыменования gcroot-> позволяет вызывать управляемый объект, используя синтаксис указателей:

m_regex = new Regex("a+");
Match* m = m_regex->Match("S[aeiou]x");

Управляемые объекты, C++ и вы живете вместе одной дружной семьей. Что тут плохого? Единственная неприятность - для использования gcroot необходим ключ /clr. Иначе откуда компилятору вообще знать, что такое gcroot/GCHandle? Из этого не следует, что код должен быть управляемым, для генерации неуправляемого кода служит директива #pragma unmanaged. Но как я уже упоминал, использование /clr влечет за собой определенные последствия. Этот ключ принудительно включает многопоточный режим (что мешает работе некоторых функций оболочки, в том числе ShellExecute) и несовместим с некоторыми ключами, например /RTC1, который заставляет компилятор добавлять полезные проверки периода выполнения на ошибки стека и переполнение буфера (см. рубрику "Отладка и оптимизация" за август 2001 г. по ссылке msdn.microsoft.com/msdnmag/issues/01/08/bugslayer). Если вы используете MFC, то, возможно, уже сталкивались с проблемами при сочетании /clr и DEBUG_NEW. Кроме того, возможны конфликты пространства имен с функциями вроде MessageBox, существующими в .NET Framework, MFC и Windows API.
В своей рубрике за январь я показал, как создать проект, где лишь один модуль использует /clr. Это отлично работает, если вызовы Framework локализованы в нескольких функциях наподобие FormatResults, которые можно поместить в отдельный файл, но ничего не получится, если классы с членами типа gcroot используются в программе повсеместно (например потому, что этот класс включен в многие другие модули директивой #include).
Итак, вы включаете /clr, и - ух! - все приложение попадает в Страну Управляемого Кода. Дело не в том, что /clr ужасен, вовсе нет, просто иногда больше подходит компиляция в неуправляемый код. Можно ли сочетать классы .NET Framework с такой компиляцией? Да, но понадобится оболочка.

ManWrap

ManWrap - это набор созданных мной инструментов для обертывания управляемых объектов в неуправляемые классы, "родные" для C++. Суть в том, что внутри себя классы используют управляемые расширения для вызова Framework, но наружу раскрывают только "родные" интерфейсы. Этот подход проиллюстрирован на рис. 3.

Рис. 3. ManWrap

Для сборки самой оболочки потребуется Managed Extensions, а для выполнения приложений - Framework, но сами приложения можно компилировать в "родном" режиме без /clr. Как ManWrap достигает этой цели? Объяснить лучше всего на примере, т. е. пошагово создавать оболочку и смотреть, что получается. Так как все .NET-объекты наследуют от Object, я начну отсюда:

class CMObject {
protected:
gcroot<Object*> m_handle;
};

CMObject - это обычный C++-класс, хранящий описатель управляемого объекта. Однако, чтобы он делал что-то полезное, мне потребуются стандартные конструкторы и некоторые операторы. Заодно я оберну довольно полезный метод Object::ToString. Рис. 4 иллюстрирует мою первую попытку.

CMObject содержит три конструктора: по умолчанию, копии и конструктор из Object*. Кроме того, он содержит оператор присваивания из CMObject и метод ThisObject, возвращающий нижележащий Object, используемый оператором разыменования ->. Этот минимум методов требуется любому классу-оболочке. Сами по себе методы (в нашем случае ToString) тривиальны:

CString CMObject::ToString() const
{
return (*this)>ToString();
}

Всего одна строчка кода, но она выполняет больше работы, чем кажется с первого взгляда: (*this)-> вызывает оператор разыменования gcroot ->, который приводит нижележащий GCHandle.Target к Object*, чей метод ToString возвращает управляемый String. Managed Extensions и IJW (It Just Works) волшебным образом преобразуют строку в LPCTSTR, автоматически используемую компилятором при конструировании CString, так как у CString есть конструктор из LPCTSTR.

Пока CMObject бесполезен, так как все, что с ним можно сделать, - создавать пустые объекты и копировать их. Но CMObject не предназначен для самостоятельного использования, это базовый класс для других оболочек. Попробуем другой класс. Capture в Framework - простой класс, представляющий одно подвыражение, совпавшее в регулярном выражении. У него три свой-ства: Index, Value и Length. Чтобы обернуть его, надо выполнить очевидное действие: Capture наследует от Object, поэтому CMCapture должен быть производным от CMObject, как показано здесь:

class CMCapture : public CMObject {
// И что теперь?
};

CMCapture унаследовал m_handle от CMObject, но m_handle - это gcroot<Object*>, а не gcroot<Capture*>. Значит, понадобится новый описатель? Нет. Capture наследует от Object, поэтому описатель gcroot<Object*> может хранить и объект Capture:

class CMCapture : public CMObject {
public:
// Вызвать базовый конструктор для инициализации m_handle
CMCapture(Capture* c) : CMObject(c) { }
};

CMCapture нужны те же конструкторы и операторы, что и CMObject; кроме того, я должен переопределить ThisObject и оператор ->, чтобы возвращать новый тип:

Capture* ThisObject() const
{
return static_cast<Capture*>((Object*)m_handle);
}

Этот static_cast безопасен, так как мой интерфейс гарантирует, что нижележащим объектом может быть только объект Capture. Обертывание новых свойств опять тривиально. Например:

int CMCapture::Index() const
{
return (*this)->Index;
}

Скрываем управляемую начинку

Пока неплохо. Похоже, мы научились "на автомате" обертывать управляемые объекты в С++. Но для успешной компиляции моих C++-классов требуется /clr. А моя задача - создать неуправляемые оболочки, чтобы приложения, их использующие, не нуждались в /clr. Чтобы избавиться от ключа /clr, надо спрятать всю управляемую начинку от неуправляемых клиентов. Например, спрятать сам описатель gcroot, так как неуправляемому коду ничего неизвестно о GCHandle. Как быть?

Один из моих учителей математики говорил, что любое доказательство - это либо плохая шутка, либо дешевый трюк. То, что я собираюсь показать, безусловно попадает в категорию дешевых трюков. Ключ к ManWrap - специальный предопределенный символ компилятора _MANAGED, равный 1 при компиляции с /clr и не определенный в ином случае. С помощью _MANAGED спрятать описатель легче легкого, как показано здесь:

#ifdef _MANAGED
# define GCHANDLE(T) gcroot<T>
#else
# define GCHANDLE(T) intptr_t
#endif

Теперь можно пересмотреть CMObject и реализовать его так:

class CMObject {
protected:
GCHANDLE(Object*) m_handle;
...
};

Модули, компилируемые с /clr (т. е. сама оболочка), видят описатель gcroot<T>. C++-приложения, скомпилированные без /clr, видят только (возможно, 64-битное) целое. Ловко, да? Я же говорил, что это дешевый фокус! Почему именно intptr_t? Дело в том, что единственное поле данных gcroot - GCHandle - специально рассчитано так, чтобы оно вело себя как целое, с преобразованиями op_Explicit в IntPtr и обратно. Intptr_t - это эквивалент IntPtr в C++, поэтому независимо от того, как вы компилируете CMObject (в управляемый или неуправляемый код), размер объекта в памяти один и тот же.

Размер имеет значение, но при компиляции в не-управляемый код важен не только он. Как насчет других особенностей управляемой начинки, например методов с комментарием "сигнатуры, содержащие управляемые типы" на рис. 4? Их тоже можно скрыть с помощью_MANAGED:

#ifdef _MANAGED
// Методы с управляемыми типами
#endif

Под "методами с управляемыми типами" я подразумеваю методы, сигнатуры которых содержат управляемые типы. Помещая их под директивами #ifdef, мы прячем их от неуправляемых клиентов. В Стране Неуправляемого Кода этих функций нет. К ним относятся конструкторы, принимающие аргумент типа X (где X - управляемый), оператор ->, бесполезный в неуправляемом коде, который к тому же нельзя скомпилировать в "родном" режиме. Эти методы нужны только внутри оболочки, компилируемой с /clr.

Итак, я спрятал описатель и все функции с управляемыми типами. Что осталось? Как насчет конструктора копии и оператора =? В их сигнатурах только "родные" типы, но реализации содержат обращение к m_handle:

class CMObject {
public:
CMObject(const CMObject& o) :
m_handle(o.m_handle) { }
};

Допустим, у меня есть CMObject obj1 и выражение вида CMObject obj2=obj1. Компилятор вызывает конструктор копии. Все отлично работает в управляемом коде, где m_handle - gcroot<Object*>, но в неуправляемом коде m_handle - это intptr_t, так что компилятор копирует целое. Ой! Копировать GCHandle как целое нельзя. Нужно перенаправить его Target или поручить сделать это gcroot. Проблема в том, что конструктор копии определен как подставляемый в код (inline). Все, что мне надо сделать, - превратить его в настоящую функцию и перенести реализацию в .cpp-файл:

// В ManWrap.h
class CMObject {
public:
CMObject(const CMObject& o);
};
// В ManWrap.cpp
CMObject::CMObject(const CMObject& o)
: m_handle(o.m_handle) {
}

Теперь при вызове конструктора копии управление передается в ManWrap.cpp, исполняемом в управляемом режиме и видящем m_handle в его истинном обличии gcroot<Object*>; gcroot присваивает значение Target, принадлежащего GCHandle. То же относится к оператору = и самим функциям-оболочкам вроде CMObject::ToString или CMCapture::Index. Функции-члены, обращающиеся к m_handle, не должны быть подставляемыми. Дополнительный вызов функции - это плата за неуправляемость. (А кому сейчас легко?) Но нельзя объять необъятное, да и издержки незна-чительны практически во всех случаях, кроме особо критичных. Если вам требуется управлять семнад-цатью миллиардами объектов в реальном времени, ни в коем случае не используйте оболочки! Но если вы хотите обратиться лишь к паре классов .NET Framework без /clr, вызов функции издержек фактически не создает.

На рис. 5 приведен окончательный вариант CMObject из ManWrap. После того как вы разберетесь с CMObject, создание новой оболочки превратится в простейший процесс: наследуете от CMObject, добавляете стандартные конструкторы и операторы, прячете управляемые сигнатуры с помощью _MANAGED и реализуете все остальное как истинные (не подставляемые) функции. Единственное отличие производных объектов в том, что их конструктор копии и оператор = можно оставить встроенными, так как они вызывают базовый класс, не обращаясь к m_handle напрямую:

class CMCapture : public CMObject {
public:
CMCapture(const CMCapture& o) : CMObject(o) { }
};

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

Следуя приведенным мной правилам, написать оболочку можно даже во сне. Но еще лучше создать пару макросов, ускоряющих процесс, как это сделал я для ManWrap. Вот окончательный CMCapture из RegexWrap.h:

class CMCapture : public CMObject
{
DECLARE_WRAPPER(Capture, Object);
public:
// Обернутые свойства/методы
int Index() const;
int Length() const;
CString Value() const;
};

Здесь, чтобы меньше стучать по клавиатуре, применяется макрос DECLARE_WRAPPER, определенный в ManWrap.h. Существует еще один макрос, IMPLEMENT_WRAPPER, делающий реализацию практически тривиальной. Макросы объявляют и реализуют все описанные основные конструкторы и операторы. Если вы не заметили, имена подобраны такие, которые знакомы MFC-программистам. В макросах DECLARE/IMPLE-MENT_WRAPPER предполагается, что вы придерживаетесь моего соглашения об именовании: CMFoo - неуправляемая оболочка управляемого класса Foo. (Я бы остановился на CFoo, но тогда возник бы конфликт с CObject из MFC для Object, так что я добавил M - Managed.) На рис. 6 показан DECLARE_WRAPPER. IMPLEMENT_WRAPPER такой же. Скачайте и изучите его в качестве домашнего задания.

Внимательные читатели могли заметить одну странную вещь. До сего момента единственными объявленными конструкторами были конструкторы по умолчанию, копии и из указателя на управляемый тип. Последний спрятан от "родного" кода, и получается, что неуправляемые клиенты могут создавать лишь пустые объекты и копировать их. Что тут полезного? Недостаток конструкторов объясняется лишь неправильным выбором классов. Object сам по себе никогда не создается, а объекты Capture поступают только от других объектов вроде Match или Group. Но Regex содержит настоящий конструктор, принимающий строку, так что CMRegex обертывает и его тоже:

// В RegexWrap.h
class CMRegex : public CMObject {
DECLARE_WRAPPER(Regex,Object);
public:
CMRegex(LPCTSTR s);
};
// В RegexWrap.cpp
CMRegex::CMRegex(LPCTSTR s)
: CMObject(new Regex(s))
{ }

Конструктор опять же должен быть настоящей функцией, так как в нем вызывается new Regex, требующий управляемых расширений и /clr. Обратите внимание, что макросы DECLARE/IMPLEMENT_WRAPPER объявляют и реализуют только канонические конструкторы и операторы, необходимые для работы с оболочками со строгим контролем типов. Если обертываемый класс содержит "настоящий" конструктор, вам придется обернуть его самостоятельно. DECLARE_WRAPPER - отличный макрос, но думать он не умеет.

При обертывании метода, возвращающего управляемый объект другого типа, вы должны обернуть и этот тип, так как очевидно, что напрямую вернуть управляемый объект в неуправляемый код не получится. Например, Regex::Match возвращает Match*, так что обертывание Regex::Match требует оболочки и для Match:

CMMatch CMRegex::Match(LPCTSTR input)
{
return CMMatch((*this)->Match(input));
}

Здесь на сцену выходит конструктор из указателя на управляемый тип. Так же, как компилятор автоматически преобразует String из Object::ToString в CString, он автоматически преобразует Match* из Regex::Match в объект CMMatch, поскольку CMMatch содержит требуемый конструктор (автоматически определенный DECLARE/IMPLEMENT_WRAPPER). Поэтому, хотя "родные" клиенты не видят конструкторы из указателя на управляемый тип, последние незаменимы для написания оболочек.

RegexWrap

После того как я рассказал о ManWrap, давайте обернем что-нибудь в честь 20-летнего юбилея "MSDN Magazine"! С помощью ManWrap я обернул класс Regex из .NET в RegexWrap.dll. На рис. 7 дан сокращенный листинг заголовочного файла.

Я не буду вдаваться в детали - все тривиально (по большей части). Вот типичная оболочка:

CString CMRegex::Replace(LPCTSTR input, LPCTSTR replace)
{
return (*this)->Replace(input, replace);
}

Практически во всех случаях реализация сводится к одной строчке: просто вызовите нижележащий управляемый метод и возложите преобразование параметров на компилятор. Ну разве interop не чудесная технология? Она работает, даже когда параметром является другой обернутый класс, как я уже объяснил в примере с CMRegex::Match.

Конечно, так не бывает, чтобы все было тривиально. На пути к RegexWrap я столкнулся со многими препятствиями: наборами, делегатами, исключениями, массивами и перечислимыми. Опишу по порядку, как я с ними справился.

Наборы

Наборы повсеместно присутствуют в Framework. Например, Regex::Matches возвращает совпадения в виде MatchCollection, а Match::Groups - группы в виде GroupCollection. Сначала я хотел преобразовывать наборы в STL-контейнеры обернутых объектов. Но, немного поразмыслив, понял, что это плохая идея. Зачем создавать новый набор описателей, указывающих на объекты, уже хранящиеся в наборе? Хотя наборы в .NET в некотором отношении напоминают STL-контейнеры, это не то же самое. Например, к GroupCollection можно обращаться как по индексу, так и по строковому имени.

Вместо вектора или карты из STL проще использовать уже созданную мной систему - ManWrap. На рис. 8 показано, как я обернул GroupCollection. Там нет ничего неожиданного за исключением нового макроса DECLARE_COLLECTION, работающего так же, как DECLARE_WRAPPER, но добавляющего три метода, имеющихся в каждом наборе: Count, IsReadOnly и IsSynchronized. Естественно, существует и IMPLEMENT_COLLECTION для их реализации. Так как GroupCollection позволяет обращаться по целому индексу или строке, у оболочки две перегруженные версии оператора [].

Обернув Match, Group и CaptureCollections, можно обернуть использующие их методы. Regex::Matches возвращает MatchCollection, и вот его оболочка:

CMMatchCollection CMRegex::Matches(LPCTSTR input)
{
return (*this)>Matches(input);
}

CMMatch::Groups и CMGroup::Captures точно такие же. И снова компилятор автоматически выполняет все преобразования. Обожаю C++ и interop!

Делегаты

Одно из наиболее заметных новшеств в истории программирования - концепция функций обратного вызова (callbacks). Они позволяют вызывать некую функцию, которая становится известной только в период выполнения. Но в управляемом мире говорят не "обратный вызов", а "делегат". Например, одна из версий Regex::Replace позволяет передать MatchEvaluator:

MatchEvaluator* delg = // создать
String *s = Regex::Replace("\\b\\w+\\b",
"Modify me. ", delg);

Regex::Replace вызывает делегат MatchEvaluator при каждом совпадении. Делегат возвращает текст для замены. Чуть позже я покажу забавный пример использования MatchEvaluator. Пока же сосредоточусь на его обертывании. То, что в Framework называется делегатами, в C++ называется функциями обратного вызова. Для их сопряжения мне первым делом потребуется typedef:

class CMMatch ... {
public:
typedef CString (CALLBACK* evaluator)(const CMMatch&,
void* param);
};

CMMatch::evaluator - это указатель на функцию, принимающий CMMatch и void* param и возвращающий CString. Включение этого typedef внутрь CMMatch диктуется в основном хорошим стилем программирования, так как предотвращает замусоривание глобального пространства имен. Void* param позволяет "родным" клиентам передавать свое состояние. С делегатами всегда связан объект (который может быть null, если метод статический), но в C/C++ у вас есть только указатель на функцию, поэтому интерфейсы обратного вызова обычно содержат void*, позволяющий передать информацию о состоянии. (C вообще заставляет вести весьма спартанскую жизнь.) С моим новым typedef я могу объявить CMRegex::Replace так:

class CMRegex ... {
public:
static CString Replace(LPCTSTR input,
LPCTSTR pattern,
CMMatch::evaluator me,
void* param);
};

Оболочка напоминает настоящий метод Replace (оба статические) с дополнительным void* param. Но как ее реализовать?

CString CMRegex::Replace(...)
{
MatchEvaluator delg = // как создать?
return Regex::Replace(..., delg);
}

Чтобы создать MatchEvaluator, мне нужен __gc-класс с методом, который вызывает неуправляемую функцию обратного вызова, используя void*, переданный клиентом. Для этого я написал маленький управляемый класс WrapMatchEvaluator (детали см. в коде для скачивания). Чтобы вы меньше стучали по клавишам, WrapMatchEvaluator содержит статическую функцию Create, возвращающую новый MatchEvaluator, так что CMRegex::Replace по-прежнему занимает лишь одну строку:

CString CMRegex::Replace(LPCTSTR input,
LPCTSTR pattern,
CMMatch::evaluator me,
void* lp)
{
return Regex::Replace(input, pattern,
WrapMatchEvaluator::Create(me, lp));
}

Ну, в исходном коде это одна строка. Мне пришлось разбить ее для печатного издания. Так как неуправляемый код не может использовать WrapMatchEvaluator (это __gc-класс), реализация помещена в RegexWrap.cpp, а не в заголовочный файл.

Исключения

Рано или поздно вы сделаете что-то такое, что не понравится .NET Framework. А что вы хотели, передавая Regex неправильное выражение? "Родной" код не может работать с управляемыми исключениями, так что надо как-то выкручиваться. Запуск CLR-отладчика не добавит мне очков, поэтому придется обернуть и исключения. Я перехватываю исключения и одеваю их в оболочки перед отправкой в Страну Неуправляемого Кода. Подход "перехвати-и-оберни" утомителен, но без него никак. Конструктор Regex может сгенерировать исключение, так что мою оболочку нужно исправить, как показано ниже:

Regex* NewRegex(LPCTSTR s)
{
try {
return new Regex(s);
} catch (ArgumentException* e) {
throw CMArgumentException(e);
} catch (Exception* e) {
throw CMException(e);
}
}
CMRegex::CMRegex(LPCTSTR s) : CMObject(NewRegex(s))
{
}

Основной прием - перехват исключения внутри оболочки и повторная генерация исключения в обернутом виде. NewRegex необходим для того, чтобы можно было использовать инициализирующий синтаксис, а не присваивать m_handle внутри конструктора (что было бы менее эффективно, так как привело бы к дву-кратному присваиванию m_handle). Благодаря перехвату и обертыванию исключений неуправляемый код становится способным обрабатывать эти исключения. Вот как ведет себя RegexTest, если пользователь вводит некорректное выражение:

// В FormatResults
try {
// Создаем CMRegex, получаем совпадения, формируем строку
} catch (const CMException& e) {
result.Format(_T("OOPS! %s\n"), e.ToString());
MessageBeep(0);
return result;
}

При обертывании исключений следует решить, какие из генерируемых исключений нуждаются в создании оболочек. Для Regex есть лишь ArgumentException, но в .NET много типов исключений. Решение о том, какие из них обертывать и сколько блоков catch добавлять, зависит от того, какой объем информации нужен вашему приложению. В любом случае всегда перехватывайте исключения базового типа в последнем блоке catch, чтобы ничто не выскользнуло наружу и не обрушило ваше приложение.

Массивы

Я победил наборы, делегаты и исключения. Пришло время массивов. Regex::GetGroupNumbers возвращает массив целых, а Regex::GetGroupNames - массив строк. Мне придется преобразовать управляемые массивы в "родные" типы до их отправки в Страну Неуправляемого Кода. Массивы в стиле C подойдут, но нет никакого смысла в их применении при наличии STL. ManWrap содержит шаблонную функцию, преобразующую управляемый массив объектов Foo в STL-вектор CMFoo. Как видите, ее вызывает CMRegex::GetGroupNames:

vector<CString> CMRegex::GetGroupNames()
{
return wrap_array<CString,String>((*this)->GetGroupNames());
}

И вновь оболочка занимает одну строку. Существует и другой wrap_array для преобразования целочисленных массивов, так как компилятору требуется ключевое слово __gc, чтобы отличать управляемые целочисленные массивы от неуправляемых. Изучение его исходного кода я оставлю вам в качестве домашнего задания.

Перечислимые

Наконец, мы подходим к перечислимым (enums), последнему трудному ребенку в семье RegexWrap. На самом деле никакой проблемы нет, просто придется слишком много набирать на клавиатуре. Некоторые методы Regex принимают RegexOptions, управляющие операцией. Например, если требуется игнорировать регистр букв, вызовите Regex::Match с параметром RegexOptions::IgnoreCase. Чтобы неуправляемые приложения могли обращаться к этим параметрам, я определил собственное "родное" перечислимое с теми же именами и значениями, что и RegexOptions (рис. 7). Чтобы избавить вас от лишнего набора на клавиатуре и исключить ошибки, я написал небольшую утилиту DumpEnum, генерирующую C-код для любого класса перечислимого в .NET Framework.

Создание DLL смешанного режима

Теперь, когда все проблемы программирования решены, осталось упаковать RegexWrap в DLL. Потребуется обычный спецификатор __declspec(dllexport) или __declspec(dllimport) для всех ваших классов; кроме того, чтобы собрать управляемую DLL, придется попотеть. Управляемым DLL нужна особая инициализация, так как в них нельзя применять обычный стартовый код DllMain. Вы должны указывать ключ /NOENTRY и выполнять инициализацию вручную. Подробности см. в моей рубрике "С++ в работе" в февральском номере за 2005 г. Главное в том, что для использования RegexWrap.dll нужно создать экземпляр специального инициализирующего класса в глобальной области видимости, как показано ниже:

// Где-то в приложении
CRegexWrapInit libinit;

Я также столкнулся с незначительной проблемой при отладке. Для отладки DLL-оболочки из неуправляемого приложения нужно установить Debugger Type в Mixed в параметрах Debug проекта. Значение по умолчанию (Auto) определяет тип загружаемого отладчика поиском в EXE-файле. В приложениях, использующих ManWrap, EXE-файл является неуправляемым, поэтому IDE запускает неуправляемый отладчик и отлаживать управляемый код нельзя. А когда вы указываете значение Mixed, IDE загружает оба отладчика.

После того как вы справитесь с этими мелкими неприятностями, RegexWrap будет работать как любая другая DLL на C++. В клиенты включают заголовочный файл и связывают с библиотекой импорта. Естественно, RegexWrap.dll должна находиться в пути PATH, а .NET Framework должна быть установлена. На рис. 9 показаны взаимосвязи между файлами и модулями в типичном клиентском приложении вроде RegexTest.

Рис. 9. Взаимосвязь между файлами и модулями

Забавы с RegexWrap

Теперь, когда Regex наконец-то обернут, пришла пора слегка позабавиться! RegexWrap создан для того, чтобы вы могли задействовать мощь регулярных выражений в неуправляемых MFC-приложениях.

Первым делом я перевел исходную версию RegexTest смешанного режима с управляемой функцией FormatResults в чисто неуправляемую версию, использующую RegexWrap. Все указатели на Regex, Match, Group и Capture теперь являются объектами CMRegex, CMMatch, CMGroup или CMCapture. То же самое касается наборов (детали см. в коде для скачивания). Важно, что RegexTest - полностью "родное" приложение, и вы не найдете ключ /clr в файлах проекта или в make-файле. Если вы не знакомы с регулярными выражениями, RegexTest - отличное средство для их изучения.

Мой следующий пример - забавная программа, превращающая английский текст в тарабарщину. Лингвисты давно заметили любопытный феномен: если перемешать буквы в середине каждого слова в предложении, но сохранить первую и последнюю буквы, результат более читабелен, чем можно было бы подумать. Наверное, при чтении наши мозги сканируют начало и конец слов, а затем заполняют их остальную часть. С помощью RegexWrap я реализовал программу WordMess, демонстрирующую этот феномен. Наберите предложение и WordMess перемешает буквы в нем по описанному правилу. На рис. 10 показан внешний вид этой программы. Вот во что WordMess превратила первое предложение этого абзаца: "Мой слидюещуй прмеир - заавнбая прраомгма, преаювщарщая анйгислкий тескт в тааррщанибу."

Рис. 10. WordMess

В WordMess используется версия Regex::Replace, принимающая делегат MatchEvaluator (конечно, обернутый):

// В CMainDlg::OnOK
static CMRegex MyRegex(_T("\\b[azAZ]+\\b"));
CString report;
m_sResult = MyRegex.Replace(m_sInput, &Scrambler, &report);

MyRegex - статический объект CMRegex, ищущий слова, т. е. последовательности из одной и более букв (без цифр), окруженных разделителями слов. (Самое сложное в написании регулярных выражений на C++ - всегда набирать два слэша вместо одного.) CMRegex::Replace вызывает функцию Scrambler для каждого найденного слова. На рис. 11 приведена функция Scrambler. Смотрите, как это легко делается с помощью STL-строк и алгоритмов swap и random_shuffle. Без STL получились бы тонны кода! Scrambler использует свой void* param как CString, к которой добавляет все сделанные замены. WordMess добавляет отчет к результатам, как показано на рис. 10. Потрясающе!

В качестве последнего примера я выбрал нечто более серьезное и полезное, а именно программу RegexForm, проверяющую различные входные данные: ZIP-код, номер карточки социального страхования, телефонный номер и C-лексемы. См. рубрику "C++ в работе" в этом выпуске, где обсуждается RegexForm.

Заключение

Вот такая оболочка! Надеюсь, что вам понравилось обертывать Regex вместе со мной и что вы используете ManWrap для обертывания других Framework-классов, которые потребуется вызывать из неуправляемого кода. ManWrap нужен не всем, а только тем, кто хочет вызывать .NET Framework из неуправляемого кода. В ином случае используйте /clr и вызывайте Framework напрямую.


Страница сайта http://test.interface.ru
Оригинал находится по адресу http://test.interface.ru/home.asp?artId=22963