Архитектура .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++. Эти правила также оказывают некоторое влияние на то, как управляемый и неуправляемый коды могут взаимодействовать друг с другом.
- 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();
// ошибка
}
Для простого и эффективного определения набора методов без определенной реализации используется управляемый интерфейс. Идея применения интерфейсов в программировании является одной из наиболее важных концепций объектно-ориентированного программирования. Интерфейсы поддерживаются в языках программирования 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
Как уже было показано в предыдущем разделе, программирование компонентов .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.
Рис. 15.1. Упаковщик, вызываемый во время совместного выполнения управляемого и неуправляемого кодов
Можно реализовать сборку упаковщика, вызываемого во время выполнения (RCW) самостоятельно, используя средство рInvoke (Platform Invocation Services, Службы обращения к платформе — они описаны далее в этой главе) для вызова необходимых интерфейсов прикладного програ\мирования (API), таких методов как CoCreatelnstance и даже самого lUnknown. Но это не является необходимостью, так как сервисная программа (утилита) Tlbimp. exe может считать информацию из библиотеки типов и автоматически сгенерировать упаковщик, вызываемый во время выполнения (RCW), что и будет описано в следующем разделе.
Сервисная программа 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), который и будет описан в этом разделе. Заметьте, что 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), все это не имеет существенного значения.
Рис. 15 2. Утилита Ildasm.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), до применения к нему утилиты Tlbimp.exe) ниже показано неуправляемое консольное клиентское приложение Win32. Взгляните на этот пример, чтобы вспомнить один из способов функционирования клиента на основе модели компонентных объектов Microsoft (COM) и сравнить его код с кодом клиента, написанным на управляемом C++" на основе модели компонентных объектов Microsoft (СОМ). (Код на управляемом C++ будет приведен в следующем подразделе.) Наш неуправляемый клиент на основе модели компонентных объектов Microsoft (СОМ) был создан с помощью Visual C++ 6.0 как консольное приложение Win32 (Win32 console application). Имеется также исполняемый (ЕХЕ) файл рабочей версии этой программы.
//LegacyCOMClient.срр
ttinclude
#include
#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.
Перед тем, как двинуться дальше и приступить к разработке программы на управляемом 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) в предыдущем разделе.
В целях сравнения ниже приведена аналогичная клиентская программа на языке С#. Конечно, эта книга посвящена 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.
Ниже описан еще один способ вызова существующего компонента, построенного на основе модели компонентных объектов Microsoft (COM), из программы на управляемом C++. В этом способе не нужно создавать сборку упаковщика с помощью Tlbimp.exe. Вместо использования Tlbimp.exe для создания сборки LegacyCOMObj, среда во время выполнения создает интерфейсный класс непосредственно из зарегистрированной библиотеки типов компонента, построенного на основе модели компонентных объектов Microsoft (COM). Заметим, что в этой версии клиента на управляемом C++ для действующего компонента на основе модели компонентных объектов Microsoft (COM) директива fusing для доступа к сборке LEGACYCOMSERVERLib. dll уже не нужна.
//ManagedCOMClientWithoutMetadata.cpp
fusing ≷mscorlib.dll>
using namespace System;
// использование пространства имен Система;
using namespace Reflection;
// использование пространства имен
Reflection (Отражение);
void main()
{
Object *args[] = {_box(3), _box (4)};
_box int *sum;
Type *type = Type::GetTypeFromProgID(
"LegacyCOMServer.LegacyCOMObj.1");
Object *object = Activator::CreateInstance(type);
// Объект
*object = Активатор:: Createlnstance (тип);
sum = static_cast<_box int *>(type->InvokeMember( // сумма
"AddEmUp", // метод, который будет вызван
BindingFlags::InvokeMethod, // флажки редактора связей
О, // объект редактора
object, // целевой объект
args // массив параметров
));
Console::WriteLine(sum); // сумма
}
Если вы запустите эту программу, то увидите выдачу, которая ничем не отличается от выдач предыдущих трех программ клиентов, основанных на модели компонентных объектов Microsoft (COM). Единственное отличие заключается в том, что в данном случае нет необходимости создавать сборку упаковщика, а потому и нет необходимости инсталлировать ее на клиентской машине (что, если подумать хорошенько, — весьма неплохо).
В целях сравнения предыдущая программа клиента на управляемом C++ реализована снова, теперь уже на С#. Новая реализация приведена ниже. Выдача новой программы, естественно, та же, что и у старой. Как и предыдущий пример, новая программа интересна прежде всего тем, что ей не нужна сборка из метаданных. Следовательно, не нужно ни добавлять ссылку к проекту, ни инсталлировать сборку. Вместо этого, информация о типе разыскивается интерпретатором динамически, т.е. во время выполнения, с помощью отражающего программного интерфейса приложения (reflection API).
//ManagedCSharpCOMClientWithoutMetadata.cs
using System;
// использование Системы;
using System.Reflection;
namespace ManagedCSharpCOMClientWithoutMetadata
// пространство имен
ManagedCSharpCOMClientWithoutMetadata
{
public class Test
// общедоступный класс Испытание
{
public static void Main(string[] args)
// общедоступная статическая Главная (строка параметров)
{
Type type; // Тип типа
Object obj; // Объект
Object[] argArray = new Object[2]; // новый Объект [2]
type = Type.GetTypeFromProgID( // Тип
"LegacyCOMServer.LegacyCOMObj.1");
obj = Activator.Createlnstance(type);
// obj = Активатор.
Createlnstance (тип);
argArray[0] = 3;
argArray[l] = 4;
Object sum = type.InvokeMember(
// Объект сумма = тип.InvokeMember (
"AddEmUp",
BindingFlags.InvokeMethod,
null, // пустой указатель
obj,
argArray
) ;
Console.WriteLine(sum); // сумма
}
}
}
Если вы запустите эту программу, то увидите выдачу, которая в точности совпадает с выдачей предыдущих программ клиентов, построенных на основе модели компонентных объектов Microsoft (COM).
Размещение и маршалинг
В приведенных нескольких примерах было рассказано, как на основе модели компонентных объектов Microsoft (COM) с помощью управляемого C++ создавать такие клиенты, которые получают доступ к существующему компоненту LegacyCOMObj, построенному на основе модели компонентных объектов Microsoft (СОМ), но совсем не уделялось внимание вопросам размещения и маршалинга. Нам не пришлось заниматься этими вопросами именно потому, что интерфейс ILegacyCOMObj имел атрибут dual (двойственный) в файле LeagcyCOM-Server.idl. Атрибут dual (двойственный) в модели компонентных объектов Microsoft (СОМ) указывает на необходимость автоматического маршалинга для интерфейса, основанного на информации из библиотеки типов. Двойственные интерфейсы встречаются очень часто, и именно они используются по умолчанию в генерируемых Мастером библиотеки шаблонных классов ATL (ATL wizard) компонентах, основанных на модели компонентных объектов Microsoft (COM). Компоненты, основанные на модели компонентных объектов Microsoft (COM), в Visual Basic 6.0 также создаются с двойственным интерфейсом. Однако если нет необходимости вызывать компоненты, основанные на модели компонентных объектов Microsoft (COM), клиентами с динамическим связыванием, реализация с помощью чисто v-табличного интерфейса может оказаться более удобной и гибкой. Таким образом, не все действующие серверы на основе модели компонентных объектов Microsoft (COM) предлагают только двойственный интерфейс. Поэтому может понадобиться маршалинг. Если его не предусмотреть, то управляемая программа клиента вызовет исключение InvalidCastException при попытке вызова метода интерфейса, для которого не указан маршалинг. Проблема состоит в том, что если клиент .NET размещен отдельно, то маршалинг для него необходим. Ниже для этой проблемы предлагается несколько решений. Заметим, что решения 1 и 2 могут оказаться неподходящими для существующих программ, особенно если у вас нет исходных текстов, или это неприемлемо для других клиентов, или невозможна повторная инсталляция.
- 1. Пометьте IDL (язык описания интерфейсов) для интерфейса как dual (двойственный) и заново реализуйте сервер на основе модели компонентных объектов Microsoft (COM).
- 2. Пометьте IDL (язык описания интерфейсов) для интерфейса как oleautomation и настройте сервер на основе модели компонентных объектов Microsoft (COM) так, чтобы все его типы параметров были дружественными к oleautomation.
- 3. Постройте и зарегистрируйте динамически подключаемую библиотеку (DLL) заместителя или заглушки для интерфейса так, чтобы выполнялся маршалинг.
- 4. Пометьте главный метод Main в клиенте на С# атрибутом [STAThreadJ или [MTAThread] (B зависимости от ситуации), чтобы поместить его в ту же потоковую модель, что и сервер на основе модели компонентных объектов Microsoft (СОМ). Например, в приведенных выше сценариях, если атрибут dual (двойственный) не используется в сервере на основе модели компонентных объектов Microsoft (COM), то для решения проблемы можно использовать атрибут [STAThread].
Очевидно, что скорее всего вам потребуется создать новое приложение .NET, в котором используются существующие компоненты, основанные на модели компонентных объектов Microsoft (COM). Однако иногда может потребоваться пройтись и в другом направлении. Например, может существовать некоторое приложение, которое использует один или несколько компонентов на основе модели компонентных объектов Microsoft (СОМ), и вы хотите переписать некоторые из этих компонентов в виде компонентов .NET, чтобы их можно было использовать в будущих решениях .NET. Однако в то же время вам может быть необходимо использовать эти новые компоненты .NET в своих существующих клиентских приложениях, основанных на модели компонентных объектов Microsoft (COM).
Программы клиентов на основе модели компонентных объектов Microsoft (COM) могут использовать раннее связывание или динамическое связывание для доступа к управляемым компонентам .NET. Для раннего связывания информация из библиотеки типов должна быть доступна во время компиляции. Для динамического связывания этого не требуется, поскольку связывание производится во время выполнения при помощи методов интерфейса IDispatch.
Однако независимо от того, использует клиент раннее или динамическое связывание, требуется мост между родной неуправляемой средой выполнения клиента на основе модели компонентных объектов Microsoft (COM) и управляемой средой выполнения компонента .NET. Этот мост называется вызываемым упаковщиком на основе модели компонентных объектов Microsoft (COM) (Callable COM Wrapper, CCW), который выступает в качестве заместителя для управляемого объекта, что и показано на рис. 15.3. Для каждого данного управляемого объекта, созданного для клиента на основе модели компонентных объектов Microsoft (COM), создается только один объект вызываемого упаковщика на основе модели компонентных объектов Microsoft (COM) (Callable COM Wrapper, CCW). Вызываемый упаковщик на основе модели компонентных объектов Microsoft (COM) (Callable COM Wrapper, CCW) управляет временем существования объектов в соответствии с правилами подсчета ссылок в Unknown, а также управляет марша-лингом вызовов метода для объекта.
Ранне-связываемые клиенты на основе модели компонентных объектов Microsoft (СОМ) обычно используют информацию библиотеки типов для доступа к компонентам на основе модели компонентных объектов Microsoft (COM). Такая информация представляет собой удобный способ создания экземпляров классов на основе модели компонентных объектов Microsoft (COM), определенных в этих компонентах, созданных на основе модели компонентных объектов Microsoft (COM). Информация а библиотеках может храниться в файлах TLB, динамически подключаемых библиотеках (DLL), специализированных управляющих элементах OLE (OCX) и исполняемых файлах, однако только файлы TLB специально предназначены именно для этой цели.
Рис. 15.3. Вызываемый упаковщик на основе модели компонентных объектов Microsoft (COM) (Callable COM Wrapper, CCW) между неуправляемым и управляемым кодами
Библиотека типов может быть сгенерирована с помощью утилиты Tlbexp.exe (Assembly to Type Library Converter — Конвертер сборки в библиотеку типов) из метаданных в сборке .NET. Это позволяет клиентам на основе модели компонентных объектов Microsoft (СОМ) просматривать компоненты .NET так, как будто они являются обычными компонентами, построенными на основе модели компонентных объектов Microsoft (COM). Утилита Tlbexp. exe (Assembly to Type Library Converter — Конвертер сборки в библиотеку типов) является дополняющей сервисной программой к утилите Tlbimp. exe, описанной в предыдущем разделе этой главы. Традиционный клиент на основе модели компонентных объектов Microsoft (COM) может использовать информацию в полученной библиотеке типов для доступа к компонентам .NET, применяя раннее связывание. Ниже приведен синтаксис для вызова утилиты командной строки Tlbexp. exe (Assembly to Type Library Converter — Конвертер сборки в библиотеку типов):
Tlbexp AssemblyName [options]
Where options may be composed of the following:
/out:FileName Output typelib file name
/nologo Prevents displaying logo
/silent Prevents displaying messages
/verbose Displays extra information
/? or /help Display usage help message
Вот более русифицированный вариант этой выдачи:
Tlbexp AssemblyName [параметры]
Где параметры могут быть составлены из следующего:
/out:FileName Имя файла выходной библиотеки типов
/nologo Подавляет вывод протокола
/ silent (тихий) Подавляет отображение сообщений
/ verbose (подробная) Дополнительная информация
/? или /help (помощь) Вывести сообщение - справку по использованию
Утилита Tlbexp. exe (Assembly to Type Library Converter — Конвертер сборки в библиотеку типов) является хорошим средством генерации полезной информации библиотеки типов для сборки .NET. Однако клиентам на основе модели компонентных объектов Microsoft (COM) необходима также запись информации в системный реестр для того, чтобы среда модели компонентных объектов Microsoft (COM) могла найти нужную фабрику классов, путь к серверу, и т.п. Утилита регистрации сборки Regasm. exe (Assembly Registration Utility) прочитывает метаданные в сборке и добавляет необходимые записи в системный реестр, что позволяет клиентам на основе модели компонентных объектов Microsoft (COM) использовать компоненты сборки .NET так, как будто они являются обычными зарегистрированными компонентами, построенными на основе модели компонентных объектов Microsoft (COM). Конечно, клиенты при этом используют заместитель — вызываемый упаковщик на основе модели компонентных объектов Microsoft (COM) (Callable COM Wrapper, CCW).
Синтаксис вызова утилиты регистрации сборки Regasm.exe приведен ниже. Эта утилита позволяет программам клиентов на основе модели компонентных объектов Microsoft (СОМ) создавать экземпляры управляемых классов, определенных в сборке.
Regasm AssemblyPath [options]
Where the options may be any of the following,
/unregister Unregister types
/tlb[:FileName] Specified typelib
/regfile[:FileName] Specified output reg file name
/codebase Sets the code base in the registry
/registered Only refer to preregistered typelibs
/nologo Prevents displaying logo
/silent Prevents displaying of messages
/verbose Displays extra information
/? or /help Display usage help message
Вот более русифицированный вариант этой выдачи:
Regasm AssemblyPath [параметры]
Где параметры могут быть любыми из следующих.
/unregister Отменить регистрацию типов
/tlb[:FileName] Указанный файл библиотеки типов
/regfile[:FileName] Указанное имя выходного файла
/codebase (кодовая страница) Устанавливает кодовую страницу
в системном реестре
/registered (зарегистрированный) Обращаться только к предварительно
зарегистрированным библиотекам
типов
/nologo Предотвращает вывод протокола
/silent (тихий) Предотвращает отображение сообщений
/verbose (подробно) Выводит дополнительную информацию
/? или /help (помощь) Вывести сообщение - справку
по использованию
Теперь давайте воспользуемся утилитами Tlbexp. exe (Assembly to Type Library Converter— Конвертер сборки в библиотеку типов) и Regasm.exe (Assembly Registration Utility — Утилита регистрации сборки) в реальном примере. Рассмотрим следующий код библиотеки классов на управляемом C++ в папке SomeManagedClass. Утилита Tlbexp.exe (Assembly to Type Library Converter — Конвертер сборки в библиотеку типов) откроет для модели компонентных объектов Microsoft (COM) только управляемые общедоступные (public) типы. Именно поэтому класс ManagedClass объявлен как public _gc (общедоступный со сборкой мусора). Кроме того, без общедоступного конструктора по умолчанию (т.е. не имеющего параметров) клиенты на основе модели компонентных объектов Microsoft (COM) не могут создавать типы при вызове CoCreatelnstance или CoCreatelnstanceEx. Таким образом, в нижеприведенном тексте класс ManagedClass имеет общедоступный конструктор по умолчанию.
Вы можете вспомнить из собственного опыта программирования, что клиент на основе модели компонентных объектов Microsoft (COM) никогда непосредственно не ссылается на СОМ-класс, а вместо этого имеет дело только с интерфейсом класса. Однако по приведенному ниже тексту нельзя сказать, что он реализует какой-либо интерфейс. Значение AutoDual (Автодуальный), которое задано в атрибуте Classlnterface (ClassInterfaceType: : AutoDual), автоматически генерирует дуальный (двойственный) интерфейс для доступа к классу ManagedClass. Это очень удобно, хотя и несколько необычно, так как не существует способа задать версию интерфейса, генерируемого со значением AutoDual (Автодуальный). Позднее мы укажем альтернативный AutoDual (Автодуальному) вариант, в котором можно будет иметь версии, но потребуется непосредственно определить по крайней мере один интерфейс
//SomeManagedClass.срр
fusing ≷mscorlib.dll>
using namespace System;
// использовать пространство имен Система;
using namespace System.:Runtime::InteropServices;
// использовать пространство имен
// Система::Время выполнения: InteropServices;
namespace SomeManagedClass
// пространство имен SomeManagedClass
{
[Classlnterfасе(ClassInterfaceType::AutoDual)] // Автодуальный
public _gc class ManagedClass
// класс сборщика мусора ManagedClass
{
public:
ManagedClass () // общедоступный конструктор по умолчанию
{
}
int AddEmUpdnt i, int 3 )
{
return i + j ;
}
},
}
В результате компиляции вышеописанной программы как проекта библиотеки классов на управляемом C++ (managed C++ Class Library) будет создана сборка — динамически подключаемая библиотека (DLL) — файл с именем SomeManagedClass . dll Следующая команда создаст из этой сборки библиотеку типов с именем
SomeManagedClass.tlb
tlbexp SomeManagedClass.dll
Можно просмотреть содержимое этого файла библиотеки типов, используя сервисную программу для просмотра объектов OLE/COM — утилиту Oleview.exe, расположенную в папке утилит SDK среды NET Это показано на рис. 15.4
Ранне-связанные клиенты на основе модели компонентных объектов Microsoft (СОМ) могут использовать данный файл SomeManagedClass .tlb во время компиляции Например, следующая консольная клиентская программа Win32 на основе модели компонентных объектов Microsoft (COM) с именем COMClientOfManagedClass (которая была создана с помощью VC++ 6 О для имитации существующего клиента на основе модели компонентных объектов Microsoft (COM)) использует раннее связывание для вызова компонентов NET в ранее описанной сборке SomeManagedClass.dll
Рис 15.4 Утилита Oleview exe показывает содержимое библиотеки типов, построенной на основе модели компонентных объектов Microsoft (COM) Библиотека типов была сгенерирована из сборки NET с помощью утилиты Tlbexp. exe (Assembly to Type Library Converter — Конвертер сборки в библиотеку типов)
//COMClientOfManagedClass cpp
ttinclude <iostream h>
#include <objbase.h>
Iimport "C \WINNT\Microsoft NET\Framework\vl.0.2914\
mscorlib.tlb"
ttimport "..\SomeManagedClass\Debug\SomeManagedClass.tlb"
no_namespace named_guids
void main()
{
{
// вложенные фигурные скобки предотвращают
// исключение указателя (pointer exception)'
CoInitialize(NULL); // ПУСТОЙ УКАЗАТЕЛЬ
_ManagedClassPtr psc(CLSID_ManagedClass);
int i = psc->AddEmUp(3, 4);
cout « i « endl « flush;
}
CoUninitialize ();
}
Однако компоненты NET будут недоступны для приложений клиентов, построенных на основе модели компонентных объектов Microsoft (COM), пока требуемые элементы не будут внесены в системный реестр и сборка не станет доступной для загрузчика классов общеязыковой среды выполнения CLR (либо локально, либо в глобальном кэше сборки) Для регистрации используется утилита регистрации сборки Regasm.exe (Assembly Registration Utility). Она читает метаданные в сборке компонентов .NET и создает соответствующие записи в системном реестре. Это позволяет любому клиенту на основе модели компонентных объектов Microsoft (COM) получать доступ к компонентам .NET так, как будто это обычные компоненты, построенные на основе модели компонентных объектов Microsoft (COM).
Например, для регистрации вышеуказанного компонента .NET как компонента на основе модели компонентных объектов Microsoft (COM) используйте следующую команду:
Regasm SomeMenagedClass.dll
Затем можно использовать утилиту Regedt32 . exe для проверки того, что информация была корректно занесена в системный реестр, что и показано на рис. 15.5.
Как только закончена регистрация сборки SomeManagedClass. dll в качестве компонента на основе модели компонентных объектов Microsoft (COM), следует инсталлировать сборку. Можно, конечно, все выполнить, просто скопировав сборку SomeManagedClass.dll в папку клиента COMClientOfManagedClass. Сделав это, можно, наконец, выполнить клиент COMClientOfManagedClass, построенный на основе модели компонентных объектов Microsoft (COM). После запуска этого клиента, как и ожидалось, будет выведено число.
Рис. 15.5. Утилита Regedt32.exe показывает содержимое системного реестра для идентификатора класса для сборки .NET, которая была зарегистрирована как компонент, построенный на основе модели компонентных объектов Microsoft (COM)
Существующие клиенты на основе модели компонентных объектов Microsoft (COM) можно динамически связать с управляемыми компонентами, так как все управляемые типы непосредственно поддерживают стандартный интерфейс модели компонентных объектов Microsoft (COM)— IDispatch. Способ, которым это выполняет общеязыковая среда выполнения CLR платформы .NET, чем-то похож на традиционное динамическое связывание в Visual Basic. Без всяких усилий в области программирования общеязыковая среда выполнения CLR немедленно генерирует реализацию IDispatch, основываясь на метаданных, обнаруженных в сборке компонента. Несмотря на то, что клиент динамического связывания может быть написан в Visual C++ 6.0 с использованием интеллектуального указателя библиотеки шаблонных классов ATL или даже непосредственного вызова методов IDispatch, проще это можно продемонстрировать, взяв клиент, который реализован с помощью короля динамического связывания — Visual Basic.
Как и для любого клиента (построенного на основе модели компонентных объектов Microsoft (COM)) компонента .NET, для клиента на VB требуется создать необходимые элементы в системном реестре для компонента .NET. Здесь можно воспользоваться утилитой регистрации сборки Regasm.exe (Assembly Registration Utility). Это требуется выполнить только для регистрации компонента. Однако утилита регистрации сборки Regasm. exe (Assembly Registration Utility) может быть использована с флагом /tlb для генерации библиотеки типов в дополнение к регистрации упомянутых типов сборки. Именно это мы собираемся сделать для следующего примера, написанного на VB, так что запустим утилиту регистрации сборки Regasm.exe (Assembly Registration Utility) еще раз, но уже с флагом /tlb:
Regasm SomeManagedClass.dll /tlb
Затем в Visual Basic можно добавить к проекту ссылку на файл SomeManagedClass.tlb. Это сделает доступным класс ManagedClass. Кроме того, обычно требуется добавить ссылку на библиотеку типов mscorlib. tlb, чтобы открыть доступ к различным типам .NET. И еще: сборку SomeManagedClass.dll нужно либо скопировать в папку клиента, либо занести в глобальный кэш сборок. Следующий фрагмент программы на VB демонстрирует динамическое связывание с компонентом .NET:
Private Sub Commandl_Click() ' Частный
Dim obj As Object ' Как Объект
Set obj = CreateObject(
"SomeManagedClass.ManagedClass") ' программный идентификатор
i = obj.AddEmUp(3, 4)
MsgBox (i) End Sub ' Конец
Введя этот текст, можно создать из него исполняемый файл ЕХЕ и запустить его на выполнение. В результате появится окно с сообщением, в котором будет выведен результат — число 7, показанное на рис. 15.6.
Рис. 15.6. Клиент Visual Basic для компонента .NET
Как строить и запускать исполняемый файл на Visual Basic
- 1. File => Make Projectl.exe.
- 2. Щелкните на ОК.
- 3. Дважды щелкните на Project.exe в Проводнике (Windows Explorer).
В предыдущем разделе мы определили общедоступный управляемый класс ManagedClass, который автоматически был представлен интерфейсом модели компонентных объектов Microsoft (COM), сгенерированным со значением AutoDual (Автодуальный), заданным в атрибуте Classlnterface (ClassInterfaceType: :AutoDual). Указанный дуальный интерфейс дал возможность клиенту получать доступ к классу ManagedClass. Это — удобная техника, хотя и несколько необычная, так как не существует способа задать версию для сгенерированного интерфейса, и создается только единственный интерфейс, который может быть в чем-то ограниченным. Здесь мы рассмотрим альтернативу значению AutoDual (Автодуальный), которая позволит создавать версии интерфейсов, но потребует точного определения интерфейса в программе. Для этого мы зададим значение None (Никакой) для атрибута Classlnterface (ClassInterfaceType: :None). Следующая программа демонстрирует управляемый компонент с явным образом описанными интерфейсом и глобально уникальными идентификаторами соклассов
//SomeManagedClass.срр
fusing ≷mscorlib.dll>
using namespace System;
// использование пространства имен Система;
using namespace System::Runtime::InteropServices;
// использование пространства имен
// Система::Время выполнения::InteropServices;
namespace SomeManagedClass
// пространство имен SomeManagedClass
{
// Интерфейс модели компонентных объектов Microsoft (COM)
[Guid("C3894DE3-F5D6-46fe-84C7-C6DDOE801C86")]
public _gc _interface IManagedClass
// сборщик мусора - интерфейс IManagedClass
{
//public:
virtual int AddEmUp(int i, int j) = 0; // виртуальный
};
//сокласс (coclass)
[Guad("8D48DE87-048E-466e-95C3-06F3C21FCEAA"),
Classlnterface(ClassInterfaceType::None)]
p die _gc class ManagedClass : public IManagedClass
{
public:
ManagedClass () // общедоступный заданный
//по умолчанию конструктор
{
}
int AddEmUp(int i, int ])
{
return i + ];
}
};
}
Следующий код (который является программой на неуправляемом C++, созданной в Visual C++ 6.0) представляет собой клиент на основе модели компонентных объектов Microsoft (COM), который работает с описанным выше управляемым компонентом. И снова глобально уникальные идентификаторы жестко запрограммированы, а не генерируются оператором импорта библиотеки типов. Таким образом, в этом примере мы не используем утилиту Tlsexp.exe для генерации библиотеки типов Это сделало пример более наглядным для ситуации, когда приходится использовать существующий клиент, разработанный на основе модели компонентных объектов Microsoft (СОМ), без каких-либо его изменений. Другими словами, мы создали компонент на управляемом C++, который будет использоваться в качестве компонента на основе модели компонентных объектов Microsoft (COM) существующим клиентом, построенным на основе модели компонентных объектов Microsoft (COM), без повторной компиляции.
//COMClientOfManagedClass.срр
#include ttinclude
// (8D48DE87-048E-466e-95C3-06F3C21FCEAA}
static const GUID CLSID_ManagedClass =
// статическая константа
{
Ox8D48DE87, Ox048E, Ox466e,
{
0x95, ОхСЗ, 0x06, OxF3, OxC2, OxlF, OxCE, OxAA
}
};
// {C3894DE3-F5D6-46fe-84C7-C6DDOE801C86}
static const GUID IID_IManagedClass =
// статическая константа
{
OxC3894DE3, OxF5D6, Ox46fe,
{
0x84, OxC7, OxC6, OxDD, OxOE, 0x80, OxlC, 0x86 )
};
class IManagedClass : public IDispatch
{
public:
virtual HRESULT _stdcall AddEmUp( // виртуальный
long a,
long b,
long* psum) = 0;
};
void main()
{
HRESULT hResult; IManagedClass *pimc;
hResult = (reinitialize (NULL) ; // ПУСТОЙ УКАЗАТЕЛЬ
if (hResult != S_OK) return;
hResult = CoCreatelnstance (
CLSID_ManagedClass,
NULL, // ПУСТОЙ УКАЗАТЕЛЬ
CLSCTX_ALL,
IID_IManagedClass,
(void **) Spimc ) ;
if (hResult == REGDB_E_CLASSNOTREG) cout
"ERROR: CLSID is not properly registered.\n"
// "ОШИБКА: CLSID должным образом не зарегистрирован \п"
flush; // сброс
if (hResult == S_OK) {
cout "CoCreatelnstance succeeded.\n"
flush; long sum; // сумма
hResult = pimc->AddEmUp(3, 4, &sum); // должно быть 6
if (hResult == S_OK)
cout « "AddEmUp(3,4) is: " « sum « endl « flush; // «сумма
endl сброс; pimc->Release();
}
CoUninitialize ();
}
Ниже приведена выдача этой программы:
CoCreatelnstance succeeded.
AddEmUp(3,4) is: 7
Press any key to continue
Службы обращения к платформе, или Plnvoke (Platform Invocation Services,), делают неуправляемые экспортируемые динамически подключаемой библиотекой (DLL) функции доступными для управляемой программы клиента. Службы обращения к платформе Plnvoke (Platform Invocation Services) позволяют сделать это для любой управляемой программы, написанной на любом языке программирования .NET. Заметим, что Plnvoke является не именем класса или метода, а мнемоническим именем для Platform Invocation Services (Службы обращения к платформе). Службы обращения к платформе Plnvoke (Platform Invocation Services) следят за маршалингом между типами данных общеязыковой среды выполнения CLR и типами собственных данных, и играют роль моста, благодаря которому преодолеваются прочие различия между управляемой и неуправляемой средой запуска программ. Несмотря на то, что службы обращения к платформе PInvoke (Platform Invocation Services) изначально использовались для доступа к интерфейсам 32-разрядных Windows-приложений (Win32 API), они могут использоваться и для вызовов унаследованных динамически подключаемых библиотек (DLL). К сожалению, применение PInvoke (Platform Invocation Services, Службы обращения к платформе) в большинстве случаев оказывается дорогой с односторонним движением. Службы обращения к платформе можно использовать для вызова из управляемой программы неуправляемых функций динамически подключаемой библиотеки (DLL) и, конечно, для возврата в управляемую программу. Службы обращения к платформе PInvoke (Platform Invocation Services) используются для доступа к глобальным экспортированным функциям динамически подключаемых библиотек (DLL), поэтому, даже если динамически подключаемая библиотека (DLL) экспортирует методы класса, они остаются недоступными через PInvoke (Platform Invocation Services, Службы обращения к платформе).
В принципе, средств C++ достаточно, чтобы даже не задумываться о существовании служб обращения к платформе PInvoke (Platform Invocation Services). Ведь в отличие от других языков .NET, Visual C++ .NET позволяет смешивать управляемый и неуправляемый код непосредственно в вашей программе. Таким образом, чтобы вызвать функцию динамически подключаемой библиотеки (DLL) из кода на управляемом C++ вы можете просто вызвать собственный код на неуправляемом C++, который, в свою очередь, обычным образом вызовет неуправляемую функцию динамически подключаемой библиотеки (DLL). Однако вы можете использовать и службы обращения к платформе PInvoke (Platform Invocation Services) для непосредственного вызова из кода на управляемом C++ неуправляемого кода динамически подключаемой библиотеки (DLL), как это показано в следующем примере кода.
//PInvoke.срр
fusing ≷mscorlib.dll>
using namespace System;
// использование пространства имен Система;
using namespace System::Runtime::InteropServices;
// использование пространства имен
// Система::Время выполнения::InteropServices;
typedef void* HWND;
[Dlllmport("user32")]
extern "C" int MessageBoxA( // интерфейс 32-разрядных
// Windows-приложений (Win32 API)
HWND hWnd, // дескриптор окна владельца
String* pText, // текст окна сообщений
String* pCaption, // заголовок окна сообщений
unsigned int uType // стиль окна сообщений
);
void main(void) {
String* pText = L"Hello PInvoke!"; // Привет PInvoke!
String* pCaption = L"PInvoke Example"; // Пример PInvoke
MessageBoxA(0, pText, pCaption, 0); }
В результате выполнения этой программы будет выведено окно сообщения, показанное на рис. 15.7.
Рис. 15.7. Это окно сообщения будет выведено при выполнении программы PInvoke.cpp
В приведенном примере PInvoke (Platform Invocation Services, Службы обращения к платформе) не показано, как службы обращения к платформе PInvoke (Platform Invocation Services) автоматически выполняют маршалинг выходных параметров. Это связано с тем, что MessageBox имеет только входные параметры. В следующем примере вызываются интерфейсы прикладных программ (API) GetComputerName и GetLastError посредством служб обращения к платформе PInvoke (Platform Invocation Services)
Первый параметр функции GetComputerName с именем IpBuffer, является выходным указателем на буфер, в который записывается строка с завершающим нулем, содержащая имя компьютера в случае успешного выполнения функции Второй параметр IpnSize является и входным, и выходным. В случае использования его в качестве входного параметра, в нем задается длина буфера IpBuffer в символах TCHAR. В случае использования его в качестве выходного параметра в нем указывается фактическая длина имени компьютера в символах TCHAR, которая содержится в IpBuffer, причем завершающий нулевой символ при подсчете длины не учитывается. Возвращаемое значение указывает, было ли успешным выполнение функции. Ненулевое значение указывает на ее успешное выполнение, а нулевое — на ошибку. Если возвращенное значение равно нулю, можно использовать функцию GetLastError для определения причины ошибки. Например, если буфер оказался недостаточно длинным для имени компьютера, эта функция вернет константу ERROR_BUFFER_OVERFLOW, имеющую значение 111. Если вы хотите попробовать самостоятельно использовать службы обращения к платформе PInvoke (Platform Invocation Services), и не будете против приложить некоторые усилия, то можете вызвать функцию интерфейса прикладных программ (API) FormatMessage для перевода этого номера ошибки в более осмысленную строку. Если вы это сделаете, и в достаточной мере укоротите длину буфера, то получите описание ошибки: "The File name is too long" ("Имя файла слишком длинное"). Прототипы функций GetComputerName и GetLastError приведены ниже:
BOOL GetComputerName( // ЛОГИЧЕСКАЯ (БУЛЕВА)
LPTSTR IpBuffer, // имя компьютера
LPDWORD IpnSize // размер буфера для имени
) ;
DWORD GetLastError(VOID) ;
В следующем примере показано, как вызвать функции GetLastError и GetComputerName из кода на управляемом C++ с помощью служб обращения к платформе РInvoke (Platform Invocation Services). Обе эти функции интерфейса прикладного программирования находятся в библиотеке Kernel32 .dll, поэтому атрибут [Dlllmport ("Kernel32" ) ] использован в обоих случаях. Заметим, что маршалинг для каждого выходного параметра выполняется автоматически. Хотя это и не показано в данном простом примере, для управления деталями маршалинга, выполняемого службами обращения к платформе PInvoke (Platform Invocation Services), можно использовать атрибуты. Впрочем, это необходимо только в том случае, если маршалинг, выполняемый по умолчанию службами обращения к платформе PInvoke (Platform Invocation Services), является неудовлетворительным.
//PInvokeOutParam.cpp
fusing ≷mscorlib.dll>
using namespace System;
// использование пространства имен Система;
using namespace System::Runtime::InteropServices;
// использование пространства имен
// Система::Время выполнения::InteropServices;
typedef int BOOL; // ЛОГИЧЕСКИЙ (БУЛЕВ)
typedef unsigned long DWORD; // без знака
#define MAX_COMPUTERNAME_LENGTH 31
[Dlllmport("Kernel32")]
extern "C" BOOL GetComputerName(
signed char *lpBuffer, UInt32* IpnSize); // символ
// со знаком
*lpBuffer
[Dlllmport("Kernel32")]
extern "C" DWORD GetLastError();
void main(void) {
signed char * IpBuffer = // символ со знаком
new signed char[MAX_COMPUTERNAME_LENGTH + 1]; // новый символ
// со знаком
UInt32 size = MAX_COMPUTERNAME_LENGTH + 1; // размер
BOOL bResult = GetComputerName(IpBuffer, Ssize);
if (bResult)
{
String *pstrComputerName = // Строка
new String((signed char *)IpBuffer); // новая Строка ((символ со знаком *)
IpBuffer);
Console::WriteLine(
"Computer Name: {0}", pstrComputerName); // Имя компьютера
}
else
{
DWORD dwLastError = GetLastError ();
Console::WriteLine(
"Last Error: {0}", _box(dwLastError)); // Последняя
// ошибка
}
}
Когда вы запустите на выполнение пример PInvokeOutParam, то увидите нечто подобное приведенному ниже, за исключением того, что будет указано реальное имя вашего компьютера, а не моего.
Computer Name: PT-2HBHVPJUGOT9
Эта глава посвящена смешиванию управляемого и неуправляемого кода с помощью Visual C++.NET. Мы рассмотрели методы вызова неуправляемым кодом управляемого кода и методы вызова управляемым кодом неуправляемого кода в рамках одного исходного файла. Затем мы рассмотрели вызов из среды .NET существующих компонентов на основе модели компонентных объектов Microsoft (COM), а также вызов из среды модели компонентных объектов Microsoft (COM) компонентов .NET, с помощью как раннего, так и динамического связывания. В заключение мы рассмотрели использование служб обращения к платформе Plnvoke (Platform Invocation Services) и увидели, как осуществляется автоматический маршалинг входных и выходных параметров.
Мы подошли к концу большого путешествия, которое, следует полагать, будет не последним путешествием по миру .NET. Мы надеемся, что оно вам понравилось. Желаем успехов в реализации ваших проектов на платформе .NET!