Программирование на Visual С++ - No.75 (Управляемый С++)
Информационный Канал Subscribe.Ru |
|
РАССЫЛКА САЙТА
RSDN.RU |
Здравствуйте, дорогие подписчики!
CТАТЬЯ Управляемый C++ Автор: Игорь Ткачёв |
C++ | CLR |
---|---|
char | Sbyte |
signed char | Sbyte |
short | Int16 |
int | Int32 |
long | Int32 |
__int64 | Int64 |
unsigned char | Byte |
unsigned short | UInt16 |
unsigned int | UInt32 |
unsigned long | UInt32 |
unsigned __int64 | UInt64 |
float | Single |
double | Double |
void | Void |
С помощью модификатора __value можно объявлять как классы и структуры, так и управляемые перечислимые типы. Более того, это единственный способ объявлять перечисления, которые будет понимать CLR. Например:
__value class cl { int a; }; __value struct st { int a; }; __value enum en : int { En1, En2 };
Всё правильно, последняя строчка не содержит ошибки. CLR поддерживает типизированные перечисления, поэтому и в MC++ вполне допустимо задавать для них тип.
Ещё одним важным отличием value-типов является то, что они не происходят от общего для CLR типов класса System::Object. Это затрудняет их использование с CLR-коллекциями и в многочисленных методах, принимающих в качестве параметра System::Object. Для разрешения использования value-типов как gc-классов в .NET используется так называемый boxing.
__box
Ключевое слово __box создаёт обёртку для value-типов, после чего их можно использовать так же, как и gc-классы. Такие языки, как C# и VB.NET, создают обёртки для value-типов автоматически, в MC++ неявное преобразование запрещено из соображений производительности.
using namespace System::Collections;
void test()
{
Stack *s = new Stack();
int i = 1;
s->Push(i); // ошибка
s->Push(__box(i)); // ok
}
Все box-value-типы, кроме перечислений, являются производными от System::ValueType, который, в свою очередь является наследником System::Object. Базовый класс для перечислений – System::Enum.
__gc pointers
Если существуют управляемые объекты, то должны существовать и управляемые указатели на такие объекты. Более того, мы уже не раз их использовали в наших примерах. Мы выяснили также, что природа управляемых и обычных C++ объектов различна, то же самое справедливо и для указателей. По аналогии с gc-массивами мы можем смело констатировать, что управляемые указатели являются самостоятельными объектами и имеют лишь внешнее сходство с регулярными указателями C++. В частности, обычный для C++ способ преобразования указателей через void* заменён для gc-типов преобразованием к System::Object*, а для value-типов к System::Void*. Среди ограничений можно отметить то, что к управляемым указателям не может быть применена адресная арифметика (вместо этого следует использовать управляемые массивы) и, как мы уже выяснили, управляемые указатели могут быть преобразованы к обычным C++ указателям только через pinning pointers.
Для преобразования одного типа управляемых указателей к другому можно использовать принятую в C++ семантику оператора dynamic_cast. В дополнение к этому MC++ определяет ещё один оператор __try_cast, основное отличие которого заключается в том, что в случае неуспеха этот оператор возбуждает исключение System::InvalidCastException. Применение операторов static_cast и reinterpret_cast также допустимо, но пользоваться ими стоит только в исключительных случаях, когда вы абсолютно уверены в том, что вы делаете. Оператор const_cast поддерживается без особых ограничений.
Интерфейсы
Необходимость появления интерфейсов в .NET вызвана, в том числе и соображениями совместимости с технологиями COM. И если CLR-объекты могут работать с Win32-кодом посредством импорта DLL, то Win32-программы имеют возможность взаимодействовать с объектами .NET только через механизм COM-интерфейсов. Кстати, эта возможность даёт нам альтернативный способ разработки COM-компонентов, который к тому же является более лёгким и приятным занятием, чем использование MFC или ATL.
Как известно, CLR не поддерживает множественного наследования классов. Возможно, это и правильно. Во-первых, не все языки его реализуют, а, во-вторых, механизм виртуальных базовых классов, обычно использующийся для разрешения конфликтов при множественном наследовании, значительно усложняет структуру таблицы виртуальных методов класса и делает вызовы виртуальных функций крайне неэффективными. С другой стороны, “облегчённый вариант” множественного наследования не порождает таких проблем и широко используется при разработке COM-компонентов вообще и с использованием библиотеки ATL в частности.
В отличие от наследования классов CLR разрешает множественное наследование интерфейсов, но это наследование и сами интерфейсы имеют ряд важных ограничений.
- все методы интерфейсов являются виртуальными и абстрактными и всегда имеют public-доступ, при этом ключевые слова virtual и public и суффикс =0 указывать не обязательно;
- методы не могут иметь реализации и должны быть переопределены в классах-наследниках;
- интерфейсы не могут содержать данных;
- интерфейсы могут быть наследниками других интерфейсов и не могут происходить от обычных классов.
Цель этих ограничений – избежать классических конфликтов при множественном наследовании, включая неоднозначность доступа к данным наследуемых объектов, и свести таблицу виртуальных методов к простому линейному массиву указателей на виртуальные функции, реализация которых будет осуществлена в наследуемых классах.
Ко всему прочему MC++ разрешает двум или более наследуемым gc-интерфейсам иметь методы с идентичными именами и параметрами. Для того чтобы избежать неоднозначности при реализации этих методов можно использовать следующий синтаксис:
__gc __interface I1 { void f(); }; __gc __interface I2 { int f(); }; __gc class Foo: public I1, public I2 { public: void I1::f() {} int I2::f() { return 0; } };
Кроме того, MC++ поддерживает для интерфейсов реализацию по умолчанию (default implementations):
__gc __interface I { void f(); }; __gc struct B { virtual void f() {}; }; __gc struct D: B, I { // по умолчанию D использует B::f для реализации I::f };
Строки
Строки в CLR представлены классом System::String и ничем особенным не выделяются среди других объектов, за исключением принятого в MC++ нового префикса для объявления строковых констант – “S”. Этот префикс обозначает управляемую строковую константу, имеющую тип System::String*, и введён для повышения производительности. Следующий код справедлив для всех трёх объявляемых строк.
#using <mscorlib.dll> using System::String; void test() { String *s1 = "123"; String *s2 = L"456"; String *s3 = S"789"; }
Доступ к управляемым строкам как к обычным неуправляемым символам может быть осуществлён следующим образом:
#include <string.h> #include <vcclr.h> using namespace System::Text; void test(System::String *s) { // wide characters wchar_t __pin *ws = PtrToStringChars(s); wcslen(ws); // ASCII characters char mas __gc[] = Encoding::ASCII->GetBytes(s); char __pin *as = &mas[0]; strlen(as); }
Делегаты и события
Упрощённо делегаты можно рассматривать как узаконенные указатели на функции. Но всё же они, как и всё в CLR, являются объектами и обладают своей дополнительной функциональностью. Объявление делегатов производится с помощью ключевого слова __delegate. С помощью делегатов могут быть вызваны любые методы управляемых классов, как обычные, так и статические. Это принципиально отличает делегаты от указателей на функции, так как делегат хранит не только указатель на функцию, но и информацию о конкретном объекте, у которого эта функция должна быть вызвана. Единственное условие – прототип метода должен совпадать с типом делегата.
__delegate void DelegateSampl(int);
__gc class Foo {
public:
void TestDelegate1(int n) {
System::Console::WriteLine(n+1);
}
static void TestDelegate2(int n) {
System::Console::WriteLine(n+2);
}
};
void test()
{
Foo *f = new Foo();
DelegateSampl *d1 = new DelegateSampl(f,Foo::TestDelegate1);
d1(1); // вызов TestDelegate1
d1 += new DelegateSampl(0,Foo::TestDelegate2);
d1(2); // одновременный вызов TestDelegate1 и TestDelegate2
}
Как следует из примера, один делегат может использоваться для обслуживания нескольких функций, т.е. делегат это не просто указатель, а список указателей. Ещё один момент – для TestDelegate2 мы опустили указание объекта, это допустимо, поскольку этот метод является статическим. Кроме того, с помощью делегатов можно вызывать даже функции Windows API, если они соответствующим образом объявлены:
using namespace System; using namespace System::Runtime::InteropServices; __delegate int MsgBox(void*,String*,String*,unsigned); __gc class Foo { public: [DllImport("user32",CharSet=CharSet::Ansi)] static int MessageBox(void*,String*,String*,unsigned); }; void test() { MsgBox *mb = new MsgBox(0,Foo::MessageBox); mb(0,S"2",S"1",0); }
Наиболее логичным применением делегатов является обработка событий, и, надо отдать должное редмондчанам, в этом вопросе они потрудились на славу. Теперь организовать генерацию событий и их обработку так же просто, как, например, переслать два байта. В дополнение к делегатам в CLR введена модель публикации/подписки на события (events). События объявляются с помощью ключевого слова __event.
__delegate void ClickEvent(int,int);
__gc class EventSource {
public:
__event ClickEvent *OnClick;
void FireEvent() {
OnClick(1,2);
}
};
Вспоминая COM трудно поверить, что это всё, что нужно для реализации механизма рассылки событий. Но не так просто обстоят дела для компилятора, фактически он генерирует примерно следующий код:
__delegate void ClickEvent(int,int); __gc class EventSource { ClickEvent *OnClick; public: // subscribe to OnClick __event void add_OnClick(ClickEvent *ce) { OnClick = static_cast<ClickEvent*>(Delegate::Combine(ce,OnClick)); } // unsubscribe to OnClick __event void remove_OnClick(ClickEvent *ce) { OnClick = static_cast<ClickEvent*>(Delegate::Remove(ce,OnClick)); } void FireEvent() { raise_OnClick(1,2); } protected: // generate notification void raise_OnClick(int x,int y) { if (OnClick) OnClick->Invoke(x,y); } // initialization EventSource() { OnClick = 0; } };
Здесь мы видим, что компилятор создаёт уже знакомый нам делегат и генерирует несколько методов, управляющих подпиской и генерацией событий. Чтобы покончить с делегатами, приведём пример, в котором участвуют источник событий и их получатель:
__delegate void ClickEvent(int,int); __gc class EventSource { public: __event ClickEvent *OnClick; void FireEvent() { OnClick(1,2); } }; __gc class EventReceiver { public: void ClickHandler(int x,int y) { Console::Write(x); } }; void test() { EventSource *es = new EventSource(); EventReceiver *er = new EventReceiver(); es->OnClick += new ClickEvent(er,EventReceiver::ClickHandler); es->FireEvent(); es->OnClick -= new ClickEvent(er,EventReceiver::ClickHandler); es->FireEvent(); }
Для подписки на события используется оператор “+=”, для её отмены “-=”.
Свойства
Свойства уже давно стали привычной вещью даже в C++, стандарт которого их всё ещё не поддерживает. Производители компиляторов на свой лад расширяют синтаксис языка, добавляя в него поддержку свойств. В таких же языках как Visual Basic и ObjectPascal, которые не слишком связаны стандартами, свойства применяются повсеместно. Технология COM, а точнее интерфейс IDispatch, также поддерживает свойства, которые с успехом используются даже скриптовыми языками. Никуда они не делись и в .NET. Для объявления свойств в MC++ служит ключевое слово __property.
__gc class Foo { public: __property int get_X() { return 0; } __property void set_X(int) {} }; void test() { Foo *f = new Foo(); f->X = 1; // вызов set_X int i = f->X; // вызов get_X }
Фактически в этом примере компилятор создаёт псевдопеременную X. Префиксы get_ и set_ являются обязательными в соответствии с соглашениями об именовании в .NET Developer Platform, но при желании мы можем использовать только один из них. Объявление свойства, которое мы рассмотрели, является скалярным объявлением и подчиняется следующим правилам:
- метод get не имеет параметров и имеет тип возвращаемого значения T;
- метод set имеет единственный параметр типа T и возвращаемый тип void;
- скалярные свойства не могут быть перегружены.
Кроме того, в CLR допустимо объявление индексируемых свойств, для которых справедливо следующее:
- метод get имеет набор параметров (T1,…,TN) и тип возвращаемого значения TR;
- метод set имеет набор параметров (T1,…,TN,TR) и возвращаемый тип void;
- индексированные свойства могут перегружаться как обычные методы.
Метаданные
Метаданные – это одно из фундаментальных понятий, на которых базируется платформа .NET. Обсудить все детали столь обширной темы в статье об MC++ просто нет никакой возможности, поэтому мы будем отталкиваться от следующего упрощения – метаданные представляют собой описания используемых в программе типов и методов в стандартном двоичном формате, хранящиеся в одном модуле вместе с кодом программы (сборке). Отдалённо это напоминает библиотеки типов из COM, но в отличие от них метаданные знают об используемых в вашей программе типах абсолютно всё. Буквально каждый ваш чих незамедлительно регистрируется в базе метаданных, будь то маленькая и скромная вспомогательная private-переменная или большой и важный public-метод. Компилятор генерирует информацию об управляемых типах автоматически, основываясь на их определении, что позволяет создавать самодостаточные в плане описания типы. Благодаря этому совершенно не важно, на каком языке программирования написан класс, от которого вы собираетесь наследоваться, и вас совершенно не должно волновать, из каких языков будет использоваться ваш код.
Вполне естественно, что значительная часть Managed Extensions for C++ отвечает за управление генерацией метаданных.
Импорт метаданных
Программа на MC++ может импортировать метаданные путём включения директивы #using специфицирующей файл, которым может быть:
- сборка .NET Developer Platform;
- .NET exe-модуль;
- obj-файл, скомпилированный с опцией /clr;
- .netmodule-файл
Следующий пример демонстрирует импорт базовых классов .NET Developer Platform.
#using <mscorlib.dll>
using namespace System;
void test()
{
}
Видимость классов
Ключевое слово public перед объявлением класса или структуры говорит компилятору, что класс будет виден любым программам, использующим для подключения сборки директиву #using. Если же класс помечен ключевым словом private, то он будет виден только внутри сборки. Это значение используется по умолчанию. Например:
__gc public class Foo1 {}; __gc private class Foo2 {};
Видимость полей и методов класса
Внешняя и внутренняя видимость членов public-классов может быть различной. Это достигается путём применения пары спецификаторов доступа из public, private и protected. Из двух спецификаторов наиболее ограничивающий используется для внешней области видимости. Порядок следования спецификаторов не важен. Например, следующий пример определяет одинаковую (только внутри сборки) область видимости для обоих методов:
public __gc class Foo { public private: void Fun1() {} private public: void Fun2() {} };
Пользовательские атрибуты
Атрибуты представляют собой универсальное средство расширения метаданных. Любой класс или его элемент может быть помечен атрибутом, информация о котором будет сохранена в метабазе. Практическое применение атрибутов мы уже видели на примере [DllImport]. Этот атрибут говорит управляющей среде, что специфицированную им функцию следует искать в модуле user32.dll. Атрибуты могут использоваться не только самой CLR или компиляторами, доступ к ним возможен из любой программы. Так же мы можем определять и свои собственные атрибуты (Custom Attributes).
Объявление пользовательского атрибута производится следующим образом:
[attribute(AttributeTargets::Class)] __gc class FooAttr: public Attribute { public: FooAttr(int,float) {} };
Все пользовательские атрибуты должны быть помечены атрибутом attribute и происходить от класса System::Attribute или его наследников. Забавно, не правда ли? Вот вам ещё одно применение атрибутов – чтобы класс стал атрибутом, нужно его пометить атрибутом attribute. В остальном это просто тип данных, информация о котором так же сохраняется в метабазе. Когда же вы применяете этот атрибут к вашим объявлениям типов, его параметры сохраняются вместе с описанием вашего типа данных. Значение перечисления AttributeTargets позволяет указывать, где синтаксически можно использовать атрибут. Определение этого перечисления выглядит следующим образом:
__value enum AttributeTargets { Assembly = 0x1, Module = 0x2, Class = 0x4, Struct = 0x8, Enum = 0x10, Constructor = 0x20, Method = 0x40, Property = 0x80, Field = 0x100, Event = 0x200, Interface = 0x400, Parameter = 0x800, Delegate = 0x1000, ReturnValue = 0x2000, All = 0x3fff, ClassMembers = 0x17fc };
Применение оператора "ИЛИ" также допускается.
Обработка исключений
Обычный механизм проверки возвращаемого значения для выявления ошибок времени выполнения постепенно уходит в прошлое. В CLR ему не нашлось места совсем. В случае возникновения любой нестандартной ситуации компоненты .NET генерируют исключения, и даже при создании обёрток для COM-объектов возвращаемые значения HRESULT преобразуются в исключения типа System::Runtime::InteropServices::COMException.
Все типы исключений в .NET имеют чёткую иерархию и происходят от базового класса System::Exception. Обычный блок try/catch может быть использован для обработки исключений как обычных типов C++, так и управляемых. Генерация исключений оператором throw тоже ничем особенным не отличается, за исключением того, что при использовании value-типов необходимо использовать boxing.
__value struct V { int v; }; void test() { try { V v; throw __box(v); } catch(__box V *ex) { } }
Когда для генерации исключения используется обычный тип C++, CLR создаёт для него обёртку типа System::Runtime::InteropServices::SEHException. Если ближайший подходящий оператор catch имеет неуправляемый тип, эта обёртка разворачивается, и обработка исключения происходит обычным для C++ образом. Это позволяет одновременно обрабатывать исключения как управляемых типов, так и неуправляемых. Но здесь есть один важный момент, если тип SEHExeption или его базовые типы встретятся первыми, то вы никогда не сможете поймать исключения неуправляемого типа. Из этого также следует, что обработчики исключений
catch(Object*)
и
catch(...)
фактически являются идентичными.
Конструкция __finally, которая введена в компилятор Visual C++ как Microsoft Specific для обработки SEH (Structured Exception Handling) исключений, также поддерживается в полном объёме и имеет ту же семантику.
Управляемые операторы
CLR поддерживает операторы, и в MC++ их объявление допустимо. Но, к сожалению, использовать обычную семантику вызова операторов нельзя из-за принятой в MC++ работы с управляемыми объектами через указатели. Тем не менее, такие языки, как C# и VB.NET, лишены этого недостатка и игнорировать такую возможность не стоит. Нельзя также использовать и ключевое слово operator для объявления операторов в управляемых классах, для этого следует пользоваться предопределёнными в CLR именами. Далее приведено соответствие между операторами и их CLR именами.
Унарные операторы
op_Decrement | -- |
op_Increment | ++ |
op_Negation | ! |
op_UnaryNegation | - |
op_UnaryPlus | + |
Бинарные операторы
op_Addition | + |
op_Assign | = |
op_BitwiseAnd | & |
op_BitwiseOr | | |
op_Division | / |
op_Equality | == |
op_ExclusiveOr | ^ |
op_GreaterThan | > |
op_GreaterThanOrEqual | >= |
op_Inequality | != |
op_LeftShift | << |
op_LessThan | < |
op_LessThanOrEqual | <= |
op_LogicalAnd | && |
op_LogicalOr | || |
op_Modulus | % |
op_Multiply | * |
op_RightShift | >> |
op_Subtraction | - |
Пример:
__value struct V { int i; static bool op_Equality(V v, int i) { return v.i == i; } static bool op_Equality(int i, V v) { return v.i == i; } };
Кроме арифметических, логических и битовых операторов, CLR поддерживает два оператора преобразования (Conversion Operators): op_Implicit и op_Explicit. Разница между ними лишь в том, что оператор op_Implicit следует применять, когда преобразование идёт без потери информации, в противном случае следует использоваться op_Explicit:
__value struct MyDouble { double d; MyDouble(int i) { d = (double) i; } static MyDouble op_Implicit(int i) { return MyDouble(i); } static int op_Explicit(MyDouble val) { return int(val.d); } };
Опции компилятора и препроцессор
Опция компилятора /clr
Для компиляции программы в управляемый код используется опция /clr. Эта опция создаёт управляемый код для всех функций, но не делает ваши классы управляемыми по умолчанию. Для этого необходимо явно использовать модификаторы __gc и __value.
#pragma unmanaged, #pragma managed
Вполне допустимо использование управляемого и неуправляемого кода в одном модуле. Прагма unmanaged заставляет генерировать компилятор неуправляемый, “родной” для используемой платформы код. Естественно, в таком коде вы не можете использовать управляемые объекты.
#pragma unmanaged void test() { printf("%d", 1) // ok Console::WriteLine(1); // ошибка } #pragma managed
_MANAGED
Этот предопределённый макрос устанавливается компилятором в 1, когда используется опция /clr. Интересно, что прагма unmanaged никак не влияет на его значение, т.е. обе следующие функции будут возвращать 1:
int test1() { return _MANAGED; } #pragma unmanaged int test2() { return _MANAGED; } #pragma managed
Опция компилятора /FAs
Эта опция не является новой в MC++, но она интересна прежде всего тем, что теперь компилятор может генерировать не только ассемблерный код, но и MSIL, “ассемблер .NET”. В частности, для последнего примера он сгенерировал MSIL для функции test1 и “родной” ассемблер для test2.
Разное
Мы уже достаточно много выяснили об MC++, но есть ещё несколько моментов, о которых следует упомянуть.
__identifier
Это ключевое слово введено в расширение для того, чтобы мы имели возможность использовать любые другие ключевые слова в качестве идентификаторов. В следующем примере мы используем класс с именем “operator”:
#using "operator.dll" void test() { __identifier(operator) *p = new __identifier(operator)(); }
__abstract
__abstract говорит компилятору о том, что наш класс или интерфейс является абстрактным и создание экземпляра этого класса не допускается, он может использоваться только как базовый класс.
__abstract __gc class Foo {};
__sealed
Это ключевое слово запрещает использовать специфицируемый класс в качестве базового класса.
__sealed __gc class Foo {};
__typeof
Оператор __typeof возвращает объект System::Type, с помощью которого можно получить исчерпывающую информацию о заданном управляемом типе.
Статические конструкторы
Управляемый класс может иметь конструктор, который будет вызван средой CLR только один раз для всех объектов данного класса. Это полезно для инициализации статических переменных класса. Порядок вызова таких конструкторов не гарантируется, но вызов всегда будет сделан до создания первого объекта данного класса.
__gc class Foo {
public:
static Foo() {}
};
Атрибут [ParamArray]
В CLR допустимо объявление функций с переменным числом параметров, но реализация этой возможности отличается от стандартного для C++ способа. На самом деле список аргументов передаётся как один параметр, являющийся управляемым массивом и помеченный атрибутом System::ParamArray. На MC++ это объявление выглядит следующим образом:
using namespace System; public __gc class Foo { public: void Fun([ParamArray] String *a[]) { } };
В C# использование атрибута ParamArray встроено в сам язык, и вместо него используется ключевое слово params:
public class Foo
{
public void Fun(params string[] a)
{
}
}
Вызов нашего метода на C# будет выглядеть следующим образом:
Foo f = new Foo(); f.Fun("1","2","3");
Т.е. фактически компилятор C# преобразует список аргументов в массив и затем передаёт его в функцию. C++ такими способностями не обладает, и нам придётся явно создавать массив, явно его инициализировать и явно передавать в функцию:
void test() { Foo *f = new Foo(); String *a[] = { S"1", S"2", S"3" }; f->Fun(a); }
Смешанный код
В отличие от других CLR-языков MC++ позволяет легко смешивать управляемый и неуправляемый код. Это представляет определённый интерес, и далее мы проведём серию смелых экспериментов для выяснения механизмов их взаимодействия. Для примера возьмём следующий текст:
static int var; void test2(); #pragma unmanaged extern "C" void test1() { var = 1; test2(); } #pragma managed void test2() { var = 1; test1(); }
Нас будут интересовать различия работы с данными, вызов управляемой функции из неуправляемого кода и наоборот. Препарируем этот текст опцией компилятора /FAs и посмотрим, что у нас получилось:
bss SEGMENT _var DD 01H DUP (?) bss ENDS EXTRN ?test1@@$$J0YAXXZ:NEAR ; test1() _test1 PROC NEAR push ebp ; { mov ebp, esp mov DWORD PTR _var,1 ; var = 1; call ?test2@@YAXXZ ; test2(); pop ebp ; } ret 0 _test1 ENDP __mep@?test2@@$$FYAXXZ TOKEN 06000004 ?test2@@YAXXZ PROC NEAR jmp DWORD PTR __mep@?test2@@$$FYAXXZ ?test2@@YAXXZ ENDP ?test2@@$$FYAXXZ: ; test2() ldc.i.1 1 ; var = 1; stsfld _var call ?test1@@$$J0YAXXZ ; test1(); ret .end ?test2@@$$FYAXXZ
Первое, на что следует обратить внимание – в одном модуле у нас нормально уживаются MSIL и ассемблер, из чего можно сделать вывод, что MC++ фактически содержит два кодогенератора. В обоих случаях вызов функций производится через специальные заглушки, что вполне понятно, единственный вопрос – это эффективность таких вызовов. Обращение к переменной происходит напрямую в обоих случаях, с той лишь разницей, что каждая функция делает это по-своему. Это лишний раз подтверждает способность CLR работать с памятью напрямую, что не совсем обычно для управляемой среды.
С вызовами всё в порядке.
При обсуждении делегатов мы рассмотрели объявление неуправляемой функции с помощью атрибута [DllImport], но теперь у нас могут возникнуть вполне законные сомнения в необходимости его применения и следующий пример это наглядно демонстрирует:
#define IServiceProvider IServiceProviderX #include <windows.h> void test() { ::MessageBox(0,"1","2",0); }
Макрос в начале примера необходим из-за конфликта имён, возникающего при подключении файла windows.h.
Идём дальше. Нам удалось успешно вызвать MessageBox из user32.dll. Зададим теперь управляемому коду более сложную задачу – прямое создание и использование COM-объектов в обход всего того, что написано в документации об интеграции .NET и COM. В качестве примера создадим объект XML DOM Document и вызовем пару его методов:
#define IServiceProvider IServiceProviderX #import <msxml.dll> void test() { ::CoInitialize(NULL); { MSXML::IXMLDOMDocumentPtr xml(__uuidof(MSXML::DOMDocument)); xml->loadXML("<root>123</root>"); Console::WriteLine((LPCTSTR)xml->documentElement->text); } ::CoUninitialize(); }
Как и ожидалось, данный код выводит на консоль строчку "123". Интересно то, что это выглядит так же, как и обычная Win32 программа, к тому же она подчиняется тем же правилам. Например, вызов CoInitialize здесь также необходим, как и для любого приложения, являющегося COM-клиентом. Можно опять усомниться в эффективности вызовов между управляемым и неуправляемым кодом, но никто не мешает нам обрамить весь этот текст прагмами unmanaged/managed и сократить накладные расходы до одного вызова неуправляемой функции. Обрамлять в данном случае стоит и саму директиву #import, так как объявление функции в неуправляемой секции заставляет компилятор генерировать для неё неуправляемый код вне зависимости от места её реализации. Например, в следующем примере мы получим ошибку компиляции (как мы знаем, неуправляемый код не может использовать управляемые объекты), хотя сама функция определена в управляемой секции.
#pragma unmanaged void test(); #pragma managed void test() { Console::WriteLine(1); // ошибка }
Заключение
Теперь пришло время ответить на наш главный вопрос: “Что же такое MC++?”. С одной стороны, вы можете смело использовать в своих программах все привычные возможности C++, шаблоны и множественное наследование, перегрузку операторов и прямую работу с памятью. Вся разница лишь в том, что компилятор будет генерировать управляемый код (MSIL) вместо ассемблера. Но! Это касается только обычных типов C++. Если же у вас возникнет необходимость (а она обязательно возникнет) в использовании gc- и value-типов, то вам не придётся заботиться об удалении объектов, CLR будет сама производить начальную инициализацию переменных и проверять допустимость значений аргументов во время исполнения. Платой за это будет следование всем ограничениям управляемой среды. Таким образом, фактически мы имеем два разных языка в одном, которые можно легко смешивать. Единственная проблема – теперь нам придётся постоянно помнить, с каким из них в данный момент мы имеем дело.
Ещё один вопрос касается терминологии. Что такое “управляемый” и “неуправляемый” код? С неуправляемым всё ясно – это обычный “родной” код Windows/Intel. С управляемыми объектами тоже понятно – CLR может их полностью контролировать. Не понятно только, как быть с обычными C++ программами, которые не используют управляемые объекты, но компилируются в MSIL код.
Пусть они тоже будут… управляемыми, хотя мы-то с вами точно знаем, что это не так :o)
Это все на сегодня. Пока!
Алекс Jenter
jenter@rsdn.ru
Duisburg, 2001. Публикуемые в рассылке материалы принадлежат сайту RSDN.
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||