программирование, создание программ, учебник Delphi, учебник по программированию, бейсек, делфи, си, паскаль
 
 
 

 

Смешивание управляемого и неуправляемого кода

Архитектура .NET поддерживает многочисленные языки программирования. В основном язык C++ выбирают из-за того, что в интерфейс 32-разрядных Windows-приложений (Win32 API), в программирование на основе модели компонентных объектов Microsoft (Component Object Model, COM) и в существующие программы были вложены большие средства. Таким образом, взаимодействие между управляемыми программами .NET общеязыковой среды выполнения CLR (Common Language Runtime) и неуправляемыми решениями и компонентами, написанными на C++, будет представлять интерес для многих программистов, во всяком случае, в обозримом будущем.
На сегодняшний день существуют различные формы взаимодействия, в том числе — протокол SOAP, который позволяет приложениям .NET вызывать Web-сервисы в различных платформах, в том числе в UNIX и в универсальных вычислительных машинах. Однако в этой главе мы остановимся на особом типе взаимодействия: установление связи (сопряжение; согласование) управляемого и неуправляемого кода в системе Windows. Преобладающими моделями программирования на C++ в современных системах Windows являются интерфейс 32-разрядных Windows-приложений (Win32 API) и модель компонентных объектов Microsoft (COM). Существует большое количество действующих компонентов, основанных на модели компонентных объектов Microsoft (COM), и было бы желательно, чтобы управляемые программы .NET могли вызывать неуправляемые компоненты, построенные на основе модели компонентных объектов Microsoft (COM). Может возникнуть и обратная ситуация, когда клиенту, построенному на основе модели компонентных объектов Microsoft (COM), требуется вызвать сервер .NET. Кроме компонентов, построенных на основе модели компонентных объектов Microsoft (COM), может возникнуть потребность в программе .NET вызвать некоторый неуправляемый код, представленный как динамически подключаемая библиотека (DLL), в том числе и интерфейс 32-разрядных Windows-приложений (Win32 API). NET Framework поддерживает все эти сценарии взаимодействия посредством средств COM Interoperability (совместимость с моделью компонентных объектов Microsoft (COM)) и Platform Invocation Services (Службы обращения к платформе), или PInvoke.
В этой главе предполагается, что вы уже знакомы с концепциями, стоящими за существующими технологиями. Кроме того, чтобы построить предлагаемые примеры программ, вам потребуется Visual Studio 6.0, равно как и Visual Studio .NET.

Сравнение управляемого и неуправляемого кода


Все остальные языки .NET, такие как VB.NET и С#, создают управляемый код, который может взаимодействовать с неуправляемыми библиотеками Win32 и компонентами на основе модели компонентных объектов Microsoft (COM). Однако язык VC++ .NET является единственным, который позволяет создавать как управляемый, так и неуправляемый код (не следует путать с ненадежным кодом в С#). Это дает возможность даже смешивать управляемый и неуправляемый коды в одном исходном файле. Управляемый код является кодом, динамически распределяемая память которого управляется автоматически (т.е. сборщиком мусора) общеязыковой средой выполнения CLR. Таким образом, программист может размещать объекты в управляемой динамически распределяемой области памяти, используя оператор new (создать), причем освобождать их с помощью соответствующих операторов delete (удалить) не нужно. Это освобождает программиста от заботы об утечках памяти, и позволяет сосредоточить основное внимание на важных и полезных задачах, таких как более точная реализация проекта программы, что повышает производительность программирования и качество программного обеспечения.
Неуправляемый код C++ .NET должен самостоятельно управлять динамически распределяемой областью в памяти традиционными способами C++, используя операторы new (создать) и delete (удалить). Так как одним из наиболее общих недостатков в программах на C++ является ужасающая утечка памяти, использование управляемых расширений VC++ .NET может оказать очень положительное воздействие на многие разработки программного обеспечения. Важно отметить, что оператор delete (удалить) может явно применяться к указателю на управляемый объект, если вы хотите самостоятельно управлять освобождением памяти, занятой объектом. Указанное обстоятельство окажется полезным в ситуациях, когда желательно выполнить деструктор объекта до того, как это сделает сам сборщик мусора, — это позволит избежать разделения . данных событий по времени в многопотоковых программах.
Причины смешивания управляемого и неуправляемого кодов
Если управляемые расширения C++ являются такими хорошими, тогда зачем может потребоваться создавать неуправляемый код? На этот вопрос существует несколько ответов:
1. Как и в других средах, где проводится автоматическая сборка мусора (таких как Smalltalk и Java), во время выполнения часто снижается производительность из-за накладных расходов на отслеживание использования объектов (отслеживание ссылок) и удаление их в нужное время.
2. Еще одним нежелательным эффектом, который часто ассоциируется с автоматической сборкой мусора, является повышение объема физической памяти, требуемой для хранения объектов, которые могут быть удалены, но еще не удалены сборщиком мусора. Более агрессивные схемы сборки мусора проигрывают в производительности, а менее агрессивные — в избыточном использовании памяти. В традиционной программе C++ программист сам решает, когда именно каждый объект удаляется из динамически распределяемой области памяти. Такой подход потенциально позволяет программисту написать программу, которая одновременно выиграет и в производительности, и в использовании памяти. К сожалению, для этого требуется большой опыт программирования и большие усилия.
3. У вас могут быть действующие приложения Win32, написанные на языке C++, которые вы хотите в течение некоторого периода времени преобразовать в приложения .NET. Тогда, по крайней мере в течение переходного периода, будет существовать программа, содержащая смесь управляемого и неуправляемого кода.
4. Вы можете обладать реальным опытом программирования в C++ и быть знакомым с традиционным программированием неуправляемого кода. Но если вам потребовалось разработать новые приложения для платформы .NET, то в этом случае вы можете захотеть написать программу, содержащую управляемые и неуправляемые части, в качестве простейшего подхода к миру программирования .NET, вместо того, чтобы нырять с головой в чистый С# или VB.NET.
Заметим, что приведенные аргументы для внедрения неуправляемого кода имеют смысл в определенных случаях, однако они применимы не ко всем ситуациям. Например, рассмотрим пункты 1 и 2 этого списка, в которых речь идет о вопросах производительности и эффективности использования памяти. В большинстве программ эти вопросы наиболее эффективно решаются за счет оптимизации относительно небольших, но критичных фрагментов программы. Таким образом, часто имеет смысл изначально реализовать программу, используя управляемые расширения (или даже С# или VB.NET), a затем, после внимательного анализа производительности, те участки программы, которые окажутся критичными, могут быть оптимизированы с использованием неуправляемого кода на C++. Независимо от того, приведет ли переработка критичных участков программы к неуправляемым реализациям или нет, — в любом случае у вас имеется выбор между использованием управляемого C++, С#, VB.NET и т.д. Несомненно, пункт 3 касается только тех случаев, когда модернизируются существующие программы. Пункт 4 имеет общий смысл для программистов на C++, которые на протяжении долгого времени совершенствовались в том, что они представляли себе как "наилучший" язык, и поэтому не хотят ни на минуту отрываться от C++. Если ни один из этих доводов не подходит к вашей ситуации, то вы можете реализовать весь проект с помощью управляемых расширений C++, или же выбрать для этого другой язык .NET.

Неуправляемый или опасный?


Язык Visual C++ .NET является практически единственным в среде .NET, который может генерировать неуправляемый код. Другие языки программирования .NET, такие как С# и VB.NET, способны генерировать только управляемый код. В частности, ключевое слово unsafe (ненадежный, опасный) в С# вообще не связано с генерацией управляемого или неуправляемого исполняемого кода программы. Ключевое слово unsafe (ненадежный, опасный) в С# освобождает среду .NET от автоматического управления памятью, разрешая использование указателей на объект.
Хотя Visual C++ .NET и является единственным языком .NET, который способен генерировать неуправляемый код, вполне возможно использовать и другие языки .NET для создания управляемого кода, который взаимодействует с неуправляемым кодом, независимо от того, является ли этот управляемый код безопасным или же ненадежным. Например, приложение .NET может вызвать неуправляемые методы СОМ-объектов посредством использования простых функциональных возможностей упаковщика, а неуправляемые функции, представленные традиционными динамически подключаемыми библиотеками (DLL), включая интерфейс 32-разрядных Windows-приложений (Win32 API), доступны посредством функции PInvoke (Platform Invocation Services, Службы обращения к платформе). Указанные возможности взаимодействия будут описаны далее в этой главе.
В Visual C++ .NET имеется выбор между созданием управляемого и неуправляемого кодов, однако нет выбора между созданием безопасного и ненадежного кода. Общеязыковая среда выполнения CLR предполагает, что все программы, написанные на C++, являются ненадежными. Подобно любой программе на С#, использующей ключевое слово unsafe (ненадежный, опасный), все программы на C++ не могут быть признаны безопасными, и таким образом могут быть выполнены только после аттестации.

Управляемые и неуправляемые ссылки и типы значений


Существуют фундаментальные отличия между тем, как управляемые и неуправляемые коды обрабатывают ссылки и типы значений. Неуправляемый код C++ позволяет объявлять локальные переменные, параметры методов и члены классов как относящиеся к типам, определенным неуправляемыми классами или структурами. Такие типы называются типами значений, так как подобные переменные содержат значения, которые в действительности являются данными. C++ также позволяет определять переменную как указатель или как ссылку на тип, определенный классом или структурой. Такие типы называются ссылочными типами, или типами ссылки, так как переменные реально не содержат значения, которые являются объектом, а вместо этого являются ссылками на объект соответствующего типа в неуправляемой динамически распределяемой области памяти. Это может немного удивить, потому что C++ пытается провести концептуальное разграничение между типами указателей и ссылочными типами. И все же в действительности ссылка в C++ является просто разновидностью постоянного (константного) указателя.
Объявление типа значения представляет собой в действительности выделение пространства памяти под реальные значения. Однако в управляемом коде нельзя объявить управляемый класс или структуру в качестве типа значения (если только не используется ключевое слово _value (значение)). Это можно увидеть в следующем примере программы, ManagedAndUnmanagedRefAndValTypes, где компилятор отметит как ошибочное объявление переменной в качестве типа значения с помощью управляемого класса ManagedClass. Для того чтобы это увидеть, попробуйте раскомментировать строку программы, содержащую оператор ManagedClass mcObj;. В результате появится сообщение об ошибке, извещающее, что вы, возможно, хотели объявить указатель, а не значение.
С другой стороны, не будет ошибкой определение переменной типа значения с помощью неуправляемого класса UnmanagedClass. Заметим, что также не будет ошибкой создание экземпляров класса ManagedClass в управляемой динамически распределяемой области памяти и экземпляров класса UnmanagedClass в неуправляемой динамически распределяемой области памяти с использованием оператора new (создать). Единственным отличием в этих случаях будет то, что для управляемого объекта не нужен оператор delete (удалить) для того, чтобы избежать утечки памяти, а для неуправляемого экземпляра такой оператор понадобится.
В нашем конкретном примере оператор delete (удалить) используется в последней строке для удаления объекта pmcobj, и комментарий утверждает, что "удалять обычно не требуется, но здесь необходимо". Оператор delete (удалить) добавлен в последнюю строку программы не потому, что непосредственная очистка требуется для управляемого объекта (сборщик мусора это сделает и без нас), а добавлен он из временных соображений. Это сделано потому, что вызов Console:: WriteLine в деструкторе управляемого класса в противном случае производился бы в самом конце выполнения программы, уже после того, как выходной поток Console (Консоль) был бы закрыт. Другими словами, если бы деструктор не был вызван явно оператором delete (удалить), управляемый объект попытался бы выполнить вывод в несуществующий поток, что вызвало бы исключение (System.ObjectDisposedException: Cannot access a closed Stream — Нельзя обращаться к закрытому потоку). Это демонстрирует наиболее общую причину явного удаления управляемого объекта, которая заключается в том, что иногда требуется явно указать момент разрушения объекта.
Хотелось бы еще обратить внимание на то, что компилятор обрабатывает примитивные типы данных (такие как int, float (с плавающей точкой), double (с удвоенной точностью), char (символ), и т.д.) не так, как управляемые классы сборщика мусора (_дс) или структуры сборщика мусора (_дс), потому что примитивные типы всегда являются типами значений.
//ManagedAndUnmanagedRefAndValTypes.срр
fusing <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
_gc class ManagedClass
// класс сборщика мусора ManagedClass
{
public:
ManagedClass()
{
Console::WriteLine("ManagedClass");
}
^ManagedClass()
{
Console::WriteLine("~ManagedClass");
}
};
_nogc class UnmanagedClass
{
public:
UnmanagedClass()
{
Console::WriteLine("UnmanagedClass"); }
-UnmanagedClass() (
Console::WriteLine("-UnmanagedClass"); } };
void main(void) {
ManagedClass *pmcObj = new ManagedClass();
//ManagedClass mcObj; // ошибка, тип значения, не допустим
UnmanagedClass *pumcObj = new UnmanagedClass() ;
delete pumcObj;
// требуется удалить из-за отсутствия сборки мусора
UnmanagedClass umcObj; // нет ошибки, тип значения допустим
int i = 3;
// нет ошибки, тип значения допустим для примитивных типов 'delete pmcObj;
// удалять обычно не требуется, но здесь необходимо
}
Вот что выведет приведенный пример программы:
ManagedClass UnmanagedClass
-UnmanagedClass UnmanagedClass
-ManagedClass
-UnmanagedClass

Ограничения на использование управляемых типов в C++


К сожалению, существует множество правил, которые ограничивают использование управляемых типов (классов, структур, интерфейсов), что ведет к затруднениям в работе с ними по сравнению с традиционными неуправляемыми типами в C++. Эти правила также оказывают некоторое влияние на то, как управляемый и неуправляемый коды могут взаимодействовать друг с другом.
1. Управляемый тип не может быть наследован из неуправляемого типа. С другой стороны, неуправляемый тип не может быть наследован из управляемого типа. Это значит, что структуры иерархий наследственности управляемых и неуправляемых классов всегда отделены друг от друга.
2. Управляемые типы не могут иметь друзей (т.е. дружественных функций, классов, структур и интерфейсов). Естественно, данное правило не распространяется на неуправляемые классы C++. Это, конечно, может быть и не так важно для многих программистов, ведь многие рассматривают дружественность как нарушение важной концепции объектно-ориентированного программирования, называемой инкапсуляцией. Неуправляемые типы могут иметь друзей, как и классы в традиционном C++; однако неуправляемые типы могут иметь друзей только из числа неуправляемых типов.
3. В отличие от неуправляемых типов, управляемые не поддерживают множественную наследуемость реализации. Однако как управляемые, так и неуправляемые типы поддерживают множественную наследуемость интерфейсов. Возможность наследовать только одну реализацию является еще одним ограничением, с которым могут смириться многие программисты. Несмотря на то, что традиционный C++ поддерживает множественную наследуемость реализаций, большинство объектно-ориентированных языков (в том числе Java и Smalltalk) ее не поддерживают. Даже модель компонентных объектов Microsoft (COM), которая на двоичном уровне основывается на таблице виртуальных функций в стиле C++, не поддерживает эту возможность.
4. Управляемый тип может, очевидно, содержать член, который является указателем на управляемый объект. Управляемый тип также может содержать элемент данных, который является неуправляемым объектом или указателем на таковой. С другой стороны, неуправляемый тип не может включать в себя экземпляр управляемого типа или указатель на таковой. Все, что здесь сказано, касается не только указателей, но также и ссылок.
5. Неуправляемый класс, в котором не указан явно базовый класс, является независимым корневым классом. В то же время, управляемый класс, в котором не указан явно ни один класс в качестве базового, является производным от корневого класса System: :Object (Система::Объект).
6. К объекту, участвующему в сборке мусора (т.е. к экземпляру управляемого класса, который использует ключевое слово _дс (сборщик мусора), а не _value (значение) или _поде), можно получить доступ только посредством указателя (или ссылки) на объект в управляемой динамически распределяемой области памяти. Это является отличием от неуправляемых типов, которые могут содержаться либо непосредственно в переменной типа значения, либо к ним можно получить доступ посредством указателя на неуправляемую динамически распределяемую область памяти.
Перечисленные правила использования управляемых типов в C++ подытожены в предлагаемом примере программы. Комментарии помогут вам понять все эти сложные правила. Если вы откроете проект ManagedAndUnmanagedTypes и попробуете раскомментировать каждый из тех операторов (лучше по одному за один проход), который вызывает ошибку компилятора, вы лучше поймете каждое из правил. Всего лишь щелкните на интересующей вас ошибке в окне Task List (Список задач), а затем нажмите F1 для получения документации, в которой разъясняется суть ошибки. И, конечно, двойной щелчок на ошибке в окне Task List (Список задач) приведет вас к соответствующему оператору в окне редактора исходного кода программы.
//ManagedAndUnmanagedTypes.срр
fusing <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
#pragma warning(disable : 4101)
// *pragma предупреждение (отключите: 4101)
// игнорировать отсутствие ссылок на локальные переменные (unref
local)
_gc class MemberManagedClass {};
// используется как элемент данных
// класс сборщика мусора MemberManagedClass {};
_nogc class MemberUnmanagedClass {};
// класс MemberUnmanagedClass используется как элемент данных
_gc class FriendManagedClass {};
// класс сборщика мусора FriendManagedClass используется как друг
_nogc class FriendUnmanagedClass {};
// класс FriendUnmanagedClass используется как друг
_gc class ManagedClass
// класс сборщика мусора ManagedClass
{
public:
MemberUnmanagedClass urn; //OK: внедренный неуправляемый MemberUnmanagedClass *pum; //OK: указатель на неуправляемый MemberUnmanagedClass &rum; //OK: ссылка на неуправляемый
//MemberManagedClass m; // Ошибка: не любит стека
MemberManagedClass *pm; //OK: указатель на управляемый
MemberManagedClass &rm; //OK: ссылка на управляемый
ManagedClass() : // необходим для инициализации ссылки
rm(*new MemberManagedClass), // требуется компилятором! rum(*new MemberUnmanagedClass), // требуется компилятором! pm(new MemberManagedClass) // не требуется компилятором
{}
// Ошибки: не может иметь никаких друзей // в управляемом классе //friend FriendManagedClass; // друг //friend FriendUnmanagedClass; // друг };
_nogc class UnmanagedClass
// класс UnmanagedClass
{
public:
MemberUnmanagedClass um; //OK: внедренный неуправляемый MemberUnmanagedClass *pum; //OK: указатель на неуправляемый MemberUnmanagedClass Srum; //OK: ссылка на неуправляемый
//MemberManagedClass m; // Ошибка: не любит стека
//MemberManagedClass *pm;
// Ошибка: управляемый* в неуправляемом
MemberManagedClass &rm; //OK: ссылка на управляемый (???)
UnmanagedClass() : // необходим для инициализации ссылки
rm(*new MemberManagedClass), // требуется компилятором! rum(*new MemberUnmanagedClass) // требуется компилятором!
{}
// Ошибка: нельзя объявить управляемый друг // в неуправляемом
//friend FriendManagedClass; // друг friend FriendUnmanagedClass; // друг - OK
};
_gc class SuperManagedClass {};
// класс сборщика мусора SuperManagedClass
_nogc class SuperUnmanagedClass {}; // класс SuperUnmanagedClass
// ошибка: управляемый тип не может происходить
//от неуправляемого типа
//_gc class BadSubManagedClass : SuperUnmanagedClass {};
// класс сборщика мусора
// BadSubManagedClass: SuperUnmanagedClass
// ошибка: неуправляемый тип не может происходить
//от управляемого типа
//_nogc class BadSubUnmanagedClass : SuperManagedClass {};
// класс BadSubUnmanagedClass: SuperManagedClass {};
//OK: can derive from machine managed/unmanaged super class
// OK: может происходить от суперкласса,
// управляемого/неуправляемого машиной
_gc class OKSubManagedClass : public SuperManagedClass {};
// класс сборщика мусора
// OKSubManagedClass: SuperManagedClass
_nogc class OKSubUnmanagedClass : SuperUnmanagedClass {};
// класс OKSubUnmanagedClass: SuperUnmanagedClass
void main(void) {
UnmanagedClass *pumc = new UnmanagedClass;
// старый C++
UnmanagedClass umc; // старый C++
UnmanagedClass srumc = *new UnmanagedClass;
// старый C++
ManagedClass *pmc = new ManagedClass;
//OK: управляемая динамически распределяемая
// область памяти
//ManagedClass me; // Ошибка: боится стека
ManagedClass &rmc = *new ManagedClass;
// OK: управляемая динамически распределяемая // область памяти
}

Вызов управляемого кода из неуправляемого и обратный вызов


Несмотря на ограничения, описанные в предыдущем разделе, есть несколько способов сотрудничества управляемого и неуправляемого кодов даже в пределах одного исходного файла. Например, приведенная ниже программа демонстрирует, что управляемый код может вызывать неуправляемый. Заметим, что можно передать указатель на элемент данных управляемого класса в качестве параметра методу неуправляемого объекта. Это оказалось возможным благодаря объявлению указателя на управляемый объект с использованием ключевого слова _pin (закрепить). Ключевое слово _pin (закрепить) закрепляет управляемый объект в памяти, запрещая его перемещение в то время, когда неуправляемый код обрабатывает данные. После запуска программы CallingFromManagedToUnmanaged на консоли вы увидите значения 0 и 1, это значит, что метод UnmanagedClassMethod успешно работает с переданным ему закрепленным управляемым объектом. Если из программы удалить ключевое слово _pin (закрепить), при компиляции обнаружится ошибка. В сообщении будет указано, что параметр, переданный в UnmanagedClassMethod нельзя преобразовать из int _gc * (который участвует в сборке мусора) Bint *.
//CallingFromManagedToUnmanaged.cpp
fusing <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
Jpragma managed
// pragma управляемый
_gc class ManagedClass
// класс сборщика мусора ManagedClass - управляемый класс
{ public:
int x;
};
tpragma unmanaged
// pragma неуправляемый
_nogc class UnmanagedClass
// класс UnmanagedClass - неуправляемый класс
{
public:
void UnmanagedClassMethod(int *px)
{
// px указывает на элемент данных х управляемого объекта
//но этот объект закреплен, поэтому неуправляемый код
// может безопасно обратиться к элементу данных х *рх = 1;
// изменяет значение, чтобы доказать,
// что это работало
}
};
ipragma managed
// pragma управляемый
void main(void)
{
ManagedClass _pin *pmcObj = new ManagedClass();
UnmanagedClass *pumcObj = new UnmanagedClass() ;
pmcObj->x = 0;
Console::WriteLine(pmcObj->x);
// до: О
// передать элемент данных управляемого объекта
// неуправляемому коду
pumcObj->UnmanagedClassMethod(&pmcObj->x);
Console::WriteLine(pmcObj->x);
// после: 1
}
Противоположный случай рассмотрен в программе CallingFromUnmanaged-ToManaged. Обратим внимание, что неуправляемый код в главной функции main вызывает управляемую функцию ManagedFunction, которая создает экземпляр управляемого класса ManagedClass и вызывает его метод ManagedClass-Method. К сожалению, неуправляемый код в главной функции main не может непосредственно создать экземпляр класса ManagedClass, так как в неуправляемом коде невозможно обратиться напрямую ни к какому управляемому типу. В этом можно убедиться, раскомментировав последний оператор, в котором создавался бы экземпляр класса ManagedClass. Но здесь компилятор обнаружит ошибку: в неуправляемой функции нельзя объявлять управляемый объект или указатель. Однако в данном примере мы видим, что управляемый код в функции ManagedFunction может создать экземпляр неуправляемого типа UnmanagedClass и передать его в качестве параметра в управляемый метод ManagedClassMethod. Итак, рассмотрим еще один способ взаимодействия управляемого и неуправляемого кода.
//CallingFromUnmanagedToManaged.cpp
#using <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
#pragma unmanaged
// pragma неуправляемый
_nogc class UnmanagedClass
// класс UnmanagedClass - неуправляемый класс
{
public:
int x; };
#pragma managed
// pragma управляемый
_gc class ManagedClass
// класс сборщика мусора ManagedClass - управляемый класс
{
public:
void ManagedClassMethod(UnmanagedClass *pumcObject) {
// pumcObject указывает на неуправляемый объект pumcObject->x =1; // изменяет значение, чтобы доказать,
// что это работало } };
void ManagedFunction() {
ManagedClass *pmcObj = new ManagedClass();
UnmanagedClass *pumcObj = new UnmanagedClass();
pumcObj->x = 0;
Console::WriteLine(pumcObj->x); //до: О
pmcObj->ManagedClassMethod(pumcObj);
Console::WriteLine(pumcObj->x); //после: 1 }
Ipragma unmanaged
// pragma неуправляемый
void main(void)
{
ManagedFunction(}; //вызов управляемого из неуправляемого //ManagedClass *pmcObj = new ManagedClass(); // ошибка
}

Сравнение программирования на C++ с использованием модели компонентных объектов Microsoft (COM) и .NET
Для простого и эффективного определения набора методов без определенной реализации используется управляемый интерфейс. Идея применения интерфейсов в программировании является одной из наиболее важных концепций объектно-ориентированного программирования. Интерфейсы поддерживаются в языках программирования Java и C++. Подобно реализации в C++ интерфейса на основе модели компонентных объектов Microsoft (COM), интерфейс .NET, реализованный на управляемом C++, содержит только общедоступные чисто виртуальные методы. Управляемый интерфейс, реализованный на управляемом C++, объявляется с одновременный указанием ключевых слов _gc (сборщик мусора) и _interface (интерфейс), как показано в приведенном ниже примере программы. Компилятор следит за наличием реализации в любом конкретном классе, реализующем такой интерфейс, что является важной гарантией с точки зрения клиентской программы. Это позволяет при написании клиентской программы использовать родовой интерфейс и не заниматься деталями вроде того, в каком конкретном классе реализован нужный интерфейс.
Следующий пример реализован как проект библиотеки классов на управляемом C++ под именем ManagedClassLibrary, который был создан с использованием последовательности меню NewoProject (Создать Проект) в Visual Studio .NET. Получившийся в результате компонент .NET — динамически подключаемая библиотека (DLL) — затем может быть вызван клиентами .NET, которые написаны на любом языке программирования, таком как VC++, С# или VB.NET. В данном проекте мы вводим пространство имен ManagedClassLibrary. Это не требуется в обязательном порядке в сборке .NET, но в больших проектах и в ситуациях, когда существует возможность использования ваших компонентов многими программистами, рекомендуется определять пространства имен для того, чтобы избежать коллизий имен.
//ManagedClassLibrary.h
ttpragma once
// pragma однажды
using namespace System;
// использование пространства имен Система;
namespace ManagedClassLibrary // пространство имен
{
public _gc _interface ISomelnterface
// сборщик мусора - интерфейс ISomelnterface
{
void SomeMethod(); int SomeOtherMethod();
};
public _gc class Somelnterfacelmpl
// класс сборщика мусора Somelnterfacelmpl
: public ISomelnterface
{
public:
void SomeMethod()
{
Console::WriteLine("SomeMethod");
}
int SomeOtherMethod()
{
Console::WriteLine("SomeOtherMethod"); return 0;
}
};
}
Приведенная программа показывает, как просто можно определить и реализовать компонент .NET. Сравним это со значительной сложностью определения и реализации на C++ традиционного компонента на основе модели компонентных объектов Microsoft (COM). Для компонента в .NET мы просто определяем интерфейс, а затем реализуем этот интерфейс в производном классе. Таким образом реализуются все преимущества и возможности, традиционно ассоциируемые с компонентно-ориентированным программированием, притом без каких-либо усложнений и усилий, которые обычно приходится прилагать при программировании с использованием модели компонентных объектов Microsoft (COM). Например, одним из достоинств модели компонентных объектов Microsoft (COM) является ее программная независимость. Обратим внимание, что программа клиента, написанная на любом языке .NET, может использовать созданный ранее компонент .NET без редактирования системного реестра и без реализации таких вещей, как фабрика классов, глобально уникальных идентификаторов (globally unique identifier, GUID) или lUnknown,.
Например, следующая программа на С# (заметьте, что мы временно переключились на язык С#) способна вызвать созданный ранее компонент .NET. Все, что требуется сделать — это создать на С# новый проект консольного приложения (назовем его ManagedClassClient), добавить в него приведенную ниже программу и добавить ссылку на проект, который ссылается на сборку ManagedClassLibrary.dll, созданную в предыдущем примере. Мы добавляем ссылку на другую сборку посредством выбора пункта меню Project => Add Reference (Проект => Добавить ссылку), затем щелкаем на кнопке Browse (Обзор) и переходим к требуемой сборке. Если вы забыли добавить ссылку на сборку, то при компиляции будет обнаружена ошибка. В сообщении об ошибке будет сказано, что пространство имен ManagedClassLibrary не существует. Посмотрев на приведенную ниже программу, вы увидите, что в ней на самом деле определяется пространство имен ManagedClassLibrary.
//ManagedClassClient.cs
using System;
// использовать Систему;
using ManagedClassLibrary;
// использовать ManagedClassLibrary;
namespace ManagedClassClient
// пространство имен ManagedClassClient
{
public class Test
// общедоступный класс Испытание
{
public static int Main(string[] args)
// общедоступная статическая Главная (строка параметров)
{
ISomelnterfасе si =
new Somelnterfacelmpl();
si.SomeMethod();
si.SomeOtherMethod();
return 0;
}
}
}
Выдача клиентской программы, написанной на С#, приведена ниже:
SomeMethod
SomeOtherMethod

Доступ из управляемого кода к компонентам, построенным на основе модели компонентных объектов Microsoft (COM)
Как уже было показано в предыдущем разделе, программирование компонентов .NET легко осуществить, используя управляемый код C++, но это так же легко и в любом другом языке .NET. Вероятно, никто ничего не потеряет, перейдя от надоевшей сложности программирования компонентов на основе модели компонентных объектов Microsoft (СОМ) к программированию компонентов .NET. Однако в этом десятилетии модель компонентных объектов Microsoft (COM) все еще останется важной технологией разработки Windows-программ. На самом деле модель компонентных объектов Microsoft (СОМ) остается основой практически любой важной новой технологии, разработанной Microsoft и другими ведущими компаниями, выпускающими программное обеспечение для Windows на протяжении нескольких последних лет. Естественно, что существует большое количество действующих компонентов и клиентских приложений на основе модели компонентных объектов Microsoft (COM). В этом разделе будет показано, как из управляемого кода на C++ вызывать методы, реализованные в компонентах на основе модели компонентных объектов Microsoft (COM). Разумеется, так же легко, используя аналогичную технологию, можно получить доступ к компонентам на основе модели компонентных объектов Microsoft (COM) и из программ, созданных на других языках .NET.
Управляемое клиентское приложение, написанное на Visual C++ .NET (или другом языке .NET), может использовать возможности взаимодействия, предусмотренные в .NET Framework для вызова существующих неуправляемых компонентов, построенных на основе модели компонентных объектов Microsoft (COM). Можно сказать, что возможности взаимодействия, предусмотренные в .NET Framework, образуют мост между средой выполнения управляемого клиента .NET и родной средой выполнения компонентов, построенных на основе модели компонентных объектов Microsoft (COM), это видно на рис. 15.1.
Можно реализовать сборку упаковщика, вызываемого во время выполнения (RCW) самостоятельно, используя средство рInvoke (Platform Invocation Services, Службы обращения к платформе — они описаны далее в этой главе) для вызова необходимых интерфейсов прикладного програ\мирования (API), таких методов как CoCreatelnstance и даже самого lUnknown. Но это не является необходимостью, так как сервисная программа (утилита) Tlbimp. exe может считать информацию из библиотеки типов и автоматически сгенерировать упаковщик, вызываемый во время выполнения (RCW), что и будет описано в следующем разделе.

Сервисная программа Tibinp. ехе


Сервисная программа Tlbimp.ere (Type Library to .NET Assembly Converter — Транслятор (конвертер) библиотеки типов на .NET) находится в папке \Program FilesXMicrosoft .NET\FrameworkSDK\Bin. Она используется для генерации управляемых классов, которые являются упаковщиками неуправляемых классов, построенных на основе модели компонентных объектов Microsoft (COM). В результате получается то, что называется упаковщиком, вызываемым во время выполнения (Runtime Callable Wrapper, RCW). Упаковщик, вызываемый во время выполнения (Runtime Callable Wrapper, RCW), представляет собой компонент .NET (т.е. управляемую сборку динамически подключаемой библиотеки (DLL)), который управляемый клиентский код может использовать для доступа к методам интерфейса модели компонентных объектов Microsoft (СОМ). Эти методы реализованы в ксмпоненте, построенном на основе модели компонентных объектов Microsoft (COM). Утилита Tlbimp.exe является программой командной строки4, которая из библиотеки типов читает информацию модели компонентных объектов Microsoft (COM), генерирует управляемый интерфейсный класс, а также соответствующие метаданные, и помедает результат в сборку упаковщика, вызываемого во время выполнения (Runtime Callable Wrapper, RCW). Полученное в сборке содержимое можно просмотреть с помощью утилиты Ildasm.exe. Ниже приведен синтаксис для вызова утилиты командной строи Tlbimp. ехе:
Tlbimp TypeLibName [options]
Where options may contain the following:
/out:FileName Assembly file name
/namespace:Namespace Assembly Namespace
/asmversion:Version Assembly version number
/reference:FileName Reference assembly
/publickey:FileName Public key file
/keyfile:FileName Key pair file
/keycontainer:FileName fey pair key container
/delaysign Eelay digital signing
/unsafe Suppress security checks
/nologo Suppress displaying logo
/silent Suppress output except errors
/verbose Display extra information
/primary Make primary interop assembly
/sysarray SAFEARRAY as System.Array
/strictref Only /reference assemblies
/? or /help Display help information
Вот более русифицированный вариант этой выдачи:
Tlbimp TypeLibName [параметры]
Где параметры могут содержать следующее:
/out:FileName Имя файла сборки
/namespace:Namespace Пространство имен сборки
/asmversion:Version Номер версии сборки
/reference:FileName Ссылки сборки
/publickey:FileName Общедоступный файл ключей
/keyfile:FileName Файл пар ключей
/keycontainer:FileName Контейнер пар ключей
/delaysign Задержка цифровой подписи
/unsafe (опасный) Подавляет проверки защиты
/nologo Подавляет вывод протокола
/silent (тихий) Подавляет вывод (кроме ошибок)
/verbose (подробная) Отображает дополнительную информацию
/primary (первичный) Сделать первичную способную
к взаимодействию сборку
/sysarray SAFEARRAY как Система.Массив /strictref Только сборки ссылок (/reference) /? или /help (справка) Выводит справочную информацию
Когда утилита Tlbimp.exe импортирует библиотеку типов, построенную на основе модели компонентных объектов Microsoft (COM), она создает пространство имен .NET с именем библиотеки, определенным в библиотеке типов (т.е. с фактическим именем библиотеки, а не с именем файла библиотеки типов, который ее содержит). Tlbimp.exe преобразует каждый сокласс (coclass) модели компонентных объектов Microsoft (COM), определенный в библиотеке типов, в управляемый интерфейсный класс .NET в результирующей сборке .NET, который имеет единственный конструктор без параметров. Tlbimp.exe маскирует каждый интерфейс модели компонентных объектов Microsoft (СОМ), определенный в библиотеке типов, под видом интерфейса .NET в результирующей сборке .NET. Рассмотрим типичный оператор файла IDL библиотеки на основе модели компонентных объектов Microsoft (COM). Этот оператор, показанный ниже, будет использоваться для создания библиотеки типов с помощью Midl.exe. При обработке файла TLB или динамически подключаемой библиотеки (DLL), созданной этим файлом IDL, утилита Tlbimp.exe сгенерирует сборку, содержащую метаданные, в том числе пространство имен LEGACYCOMSERVERLib, управляемый интерфейсный класс LegacyCOMObj и управляемый интерфейс ILegacyCOMObj.
library LEGACYCOMSERVERLib
// библиотека LEGACYCOMSERVERLib
{
coclass LegacyCOMObj
{
[default] interface ILegacyCOMObj;
// [заданный по умолчанию] интерфейс ILegacyCOMObj;
};
};
После того, как вы использовали Tlbimp.exe для генерации сборки упаковщика, можно просмотреть его содержи мое при помощи утилиты lldasm.exe.
Tlbimp LegacyCOMServer.tlb
Ildasm LEGACYCOMSERVERLib.dll
Эта команда отобразит содержимое сборки упаковщика, как показано на рис. 15.2. Обратите внимание, что утилита lldasm.exe в качестве имени пространства имен выводит LEGACYCOMSERVERLib, в качестве имени интерфейса— ILegacyCOMObj, а в качестве имени интерфейсного класса — LegacyCOMObj. В следующем подразделе мы посмотрим на исходный код этого унаследованного компонента, построенного на основе модели компонентных объектов Microsoft (COM).

Унаследованный компонент на основе модели компонентных объектов Microsoft (COM)
В целях демонстрации нам потребовался действующий компонент на основе модели компонентных объектов Microsoft (COM), который и будет описан в этом разделе. Заметьте, что IDL-файл LegacyCOMServer был создан как часть проекта динамически подключаемой библиотеки VC++ 6.0 (ATL СОМ AppWizard DLL). Этот проект содержит сгенерированный Мастером создания объектов на основе библиотеки шаблонных классов ATL (ATL Object Wizard) объект LegacyCOMObj, содержащий один дуальный интерфейс без агрегирования. Этот дуальный интерфейс имеет один метод с именем AddEmUp, который принимает два входных параметра целого типа с именами i и j и один выходной параметр типа int* с именем psum.
Как создать и использовать упаковщик, вызываемый во время выполнения (Runtime Callable Wrapper, RCW)
1. Имея файл динамически подключаемой библиотеки (DLL) — сервер, построенный на основе модели компонентных объектов Microsoft (COM), — или TLB-файл, с помощью Tlbimp.exe создайте упаковщик, вызываемый во время выполнения (Runtime Callable Wrapper, RCW). Этот упаковщик позволит получить доступ из управляемого клиентского кода к компонентам на основе модели компонентных объектов Microsoft (COM).
2. С помощью Regsvr32.exe зарегистрируйте на своей машине динамически подключаемую библиотеку (DLL) сервера на основе модели компонентных объектов Microsoft (COM), если это не было сделано раньше.
3. При желании, добавьте оператор использования пространства имен в клиентскую управляемую программу на C++, чтобы можно было обращаться к классу на основе модели компонентных объектов Microsoft (COM) по его короткому имени. Это пространство имен можно найти с помощью Oleview.exe — нужно поискать имя библиотеки на сервере. Несомненно, следует также добавить в клиентскую программу фрагмент, который объявляет и вызывает методы компонента на основе модели компонентных объектов Microsoft (COM). Пример такого фрагмента мы рассмотрим немного позже.
4. Инсталлируйте сборку LegacyCOMServer.dll, если вы еще этого не сделали. Проще всего с этой целью скопировать файл LegacyCOMServer.dll в папку управляемого клиента. Теперь клиентская программа может быть скомпилирована и запущена для проверки правильности работы упаковщика, вызываемого во время выполнения (Runtime Callable Wrapper, RCW).
Если вы хотите создать проект ATL СОМ самостоятельно, но у вас нет Visual Studio 6.0, создайте новый проект на основе библиотеки шаблонных классов ATL в Visual, Studio.NET. Однако в этом случае вы обнаружите, что сгенерированный код пусковой системы радикально отличается от приведенного в нашем примере кода, который был получен с помощью Visual Studio 6.0. Например, не существует файла IDL, а вместо этого используется ключевое слово _interface (интерфейс) для определения интерфейса ILegacyCOMOb] непосредственно в файле LegacyCOMOb j . h. Кроме того, Visual C++.NET в сгенерированном коде существенно использует атрибуты, и поэтому такой код даже по виду отличается от кода на обычном Visual C++. Однако, в целях обучения использованию Tlbimp.exe для компонентов на основе модели компонентных объектов Microsoft (COM), все это не имеет существенного значения.
import "oaidl.idl"; // импорт
import "ocidl.idl"; // импорт
[
object, // объект
uuid(7C82D19B-2B04-476B-AEC8-OABFD7A2E54B), dual, // двойной
helpstring("ILegacyCOMOb] Interface"), // Интерфейс
pointer_default(unique) // уникальный
]
interface ILegacyCOMOb] : IDispatch // интерфейс
{
[id(l), helpstring("method AddEmUp")] // идентификатор,
// метод
HRESULT AddEmUp([in] int i, [in] int ],
[out, retval] int *psum); }; [
uuid(5FBA2BCl-CD8B-4B20-AF94-4CA17714C9CO),
version(1.0), // версия
helpstring("LegacyCOMServer 1.0 Type Library") // Библиотека
// типов ] library LEGACYCOMSERVERLib // библиотека
{
importlib("stdole32.tlb"); importlib("stdole2.tlb");
[
uuid(EBAC6FDO-D55B-4BA6-B386-8B774255A87C) ,
helpstring("LegacyCOMObj Class") // Класс
]
coclass LegacyCOMObj
{
[default] interface ILegacyCOMObj;
// [заданный по умолчанию] интерфейс ILegacyCOMObj;
};
};
Вышеприведенный файл IDL является частью такого же проекта на основе библиотеки шаблонных классов ATL LegacyCOMServer, который содержит следующий код реализации для открытого метода AddEmUp интерфейса на основе модели компонентных объектов Microsoft (COM):
STDMETHODIMP CLegacyCOMObj::AddEmUp(int i, int j, int *psum)
{
// TODO: Add your implementation code here
// TODO: Добавьте ваш код реализации здесь
*psum = i + j;
return S_OK;
}
Когда построен вышеуказанный проект сервера на основе модели компонентных объектов Microsoft (COM), системный реестр будет автоматически обновлен на одном из шагов построения. Однако если вы инсталлируете проект сервера на другой машине, то там тоже придется его зарегистрировать. Это может быть выполнено из командной строки следующим образом:
Regsvr32 LegacyCOMServer.dll
Рабочая версия этой динамически подключаемой библиотеки (DLL), не зависящая от библиотеки шаблонных классов ATL во время выполнения, прилагается. Вы можете ее зарегистрировать при помощи пакетного файла reg.bat и отменить регистрацию посредством unreg. bat.

Действующий клиент на основе модели компонентных объектов Microsoft (COM)
В целях сравнения (и, конечно, для тестирования компонента, построенного на основе модели компонентных объектов Microsoft (COM), до применения к нему утилиты Tlbimp.exe) ниже показано неуправляемое консольное клиентское приложение Win32. Взгляните на этот пример, чтобы вспомнить один из способов функционирования клиента на основе модели компонентных объектов Microsoft (COM) и сравнить его код с кодом клиента, написанным на управляемом C++" на основе модели компонентных объектов Microsoft (СОМ). (Код на управляемом C++ будет приведен в следующем подразделе.) Наш неуправляемый клиент на основе модели компонентных объектов Microsoft (СОМ) был создан с помощью Visual C++ 6.0 как консольное приложение Win32 (Win32 console application). Имеется также исполняемый (ЕХЕ) файл рабочей версии этой программы.
//LegacyCOMClient.срр
ttinclude <iostream.h>
#include <objbase.h>
#import "..\LegacyCOMServe\LegacyCOMServer.tlb" no_namespace named_guids
void main()
{
{
// вложенные фигурные скобки
// предотвращают исключение указателя (pointer exception)!
Colnitialize(NULL); // ПУСТОЙ УКАЗАТЕЛЬ
ILegacyCOMObjPtr pi(CLSID_LegacyCOMObj) ;
int i = pi->AddEmUp(3, 4);
cout « i « endl « flush;
}
CoUninitialize();
}
Выдача этого клиентского приложения, построенного на основе модели компонентных объектов Microsoft (COM), приведена ниже. Она получена в результате вызова интерфейсного метода AddEmUp, построенного на основе модели компонентных объектов Microsoft (COM). В качестве параметров методу передаются числа 3 и 4.

Создание клиента на основе модели компонентных объектов Microsoft (COM) с помощью управляемого C++
Перед тем, как двинуться дальше и приступить к разработке программы на управляемом C++, которая сможет выступать в роли клиента для имеющегося компонента, построенного на основе модели компонентных объектов Microsoft (COM), мы создадим сборку LEGACYCOMSERVERLib.dll, применив утилиту Tlbimp.exe к файлу LegacyCOMSErver. tlb. Мы уже делали подобное раньше, однако здесь для удобства повторим описание всех необходимых действий. Заметим, что утилиту Tlbimp.exe можно применить к файлу TLB или к динамически подключаемой библиотеке (DLL), содержащей компоненты, построенные на основе модели компонентных объектов Microsoft (СОМ). В предлагаемой программе ManagedCOMClient. срр предполагается, что она выполняется в той же папке, в которой находится файл LECACYCOMSERVERLib. dll (для того, чтобы не менять оператор fusing в клиентской программе). Tlbimp LegacyCOMServer.dll
После этого требуется создать сборку, пригодную для загрузчика классов общеязыковой среды выполнения CLR (т.е. сборку необходимо инсталлировать). Одним из способов инсталляции компонента .NET является его копирование в папку клиентской программы. Это известно как "локальная инсталляция". Но сначала мы должны создать клиентскую программу ManagedCOMClient на управляемом C++, которая использует наш компонент, построенный на основе модели компонентных объектов Microsoft (COM). Текст этой программы приведен ниже:
//ManagedCOMClient.срр
#using <mscorlib.dll>
using namespace System;
// использование пространства имен Система;
#using <..\LegacyCOMServer\LEGACYCOMSERVERLib.dll>
using namespace LEGACYCOMSERVERLib;
// использование пространства имен
LEGACYCOMSERVERLib; // имя библиотеки в языке описания интерфейса (IDL)
void main()
{
ILegacyCOMObj *plco;
// от названия интерфейса в языке описания интерфейса (IDL)
plco = new LegacyCOMObj;
// от названия сокласса (coclass) в языке
// описания интерфейса (IDL)
int sum = plco->AddEmUp(3, 4); // суммируем
Console::WriteLine(sum); // сумма
}
Перед запуском этой программы нужно убедиться, что сборка LEGACYCOMSERVERLib.dll скопирована в папку с выполняемой программой. В противном случае общеязыковая среда выполнения CLR сгенерирует исключение System. 10. FileNotFoundException при попытке загрузки интерфейса I'LegacyCOMObj из сборки, которую ей найти не удастся. Выдача этого клиентского приложения, разработанного на основе модели компонентных объектов Microsoft (COM) с помощью управляемого C++, в точности совпадает с выдачей клиента, разработанного на основе модели компонентных объектов Microsoft (COM) в предыдущем разделе.

Разработка управляемого клиента на основе модели компонентных объектов Microsoft (COM) с помощью С#
В целях сравнения ниже приведена аналогичная клиентская программа на языке С#. Конечно, эта книга посвящена C++, а не С#, однако некоторые фрагменты программ на С# помещены в нее для наглядности. Программа на С# в точности соответствует программе на управляемом C++, но чуточку проще. Чтобы она работала, необходимо добавить в проект ссылку на сборку LEGACYCOMSERVERLib.dll, созданную при помощи утилиты Tlbimp.exe. Добавление ссылки к проекту на С# эквивалентно использованию директивы fusing в программе на управляемом C++. Нет необходимости сейчас копировать сборку LEGACYCOMSERVERLib.dll, так как это выполнится автоматически после добавления ссылки к проекту Visual Studio.NET.
//ManagedCSharpCOMClient.cs
using System;
// использование Системы;
using LEGACYCOMSERVERLib;
// использование LEGACYCOMSERVERLib;
namespace ManagedCSharpCOMClient
// пространство имен ManagedCSharpCOMClient
{
public class Test
// общедоступный класс Испытание
{
public static void Main(string[] args)
// общедоступная статическая Главная (строка параметров)
{
LegacyCOMObj Ico; // интерфейс
Ico = new LegacyCOMObj(); //coclass
int sum = Ico.AddEmUp(3, 4);// суммировать
Console.WriteLine(sum); // сумма
}
}
}
Если вы запустите эту программу, то увидите выдачу, в точности совпадающую с выдачей предыдущих клиентских программ LegacyCOMClient и ManagedCOMClient.

 

Резюме


Эта глава посвящена смешиванию управляемого и неуправляемого кода с помощью Visual C++.NET. Мы рассмотрели методы вызова неуправляемым кодом управляемого кода и методы вызова управляемым кодом неуправляемого кода в рамках одного исходного файла. Затем мы рассмотрели вызов из среды .NET существующих компонентов на основе модели компонентных объектов Microsoft (COM), а также вызов из среды модели компонентных объектов Microsoft (COM) компонентов .NET, с помощью как раннего, так и динамического связывания. В заключение мы рассмотрели использование служб обращения к платформе Plnvoke (Platform Invocation Services) и увидели, как осуществляется автоматический маршалинг входных и выходных параметров.
Мы подошли к концу большого путешествия, которое, следует полагать, будет не последним путешествием по миру .NET. Мы надеемся, что оно вам понравилось. Желаем успехов в реализации ваших проектов на платформе .NET!

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика