Взаимодействие с неуправляемым кодом
ГЛАВА 17
Взаимодействие с неуправляемым кодом
Продолжительность жизни нового языка или среды разработки сильно ограничена, если он или она игнорируют унаследованные системы и программы, предоставляя лишь средства для написания новых систем. Независимо от привлекательности новой технологии ее создатели должны учесть, что какое-то время ей придется сосуществовать со старой технологией. Поэтому команды разработчиков .NET и С# решили облегчить программистам взаимодействие с существующим кодом посредством неуправляемого (unmanaged) кода, т. е. кода, которым нельзя управлять (контролировать) средствами поддержки периода выполнения .NET. В этой главе я расскажу о трех основных примерах неуправляемого кода в .NET, а именно:
- Platform Invocation Services позволяют коду .NET обращаться к функциям, структурам и даже к обратным вызовам в существующих неуправляемых библиотеках DLL;
- небезопасный код (unsafe code) позволяет С# -программисту использовать в приложениях такие конструкции, как указатели, отдавая управление этим кодом на откуп исполняющей среде .NET;
- взаимодействие с COM (COM interoperability) под этим понимается способность кода .NET применять компоненты СОМ, а приложений СОМ — компоненты .NET.
Службы Platform Invocation Services .NET (или PInvoke) позволяют управляемому коду работать с функциями и структурами, экспортированными из DLL. В этом разделе мы увидим, как вызывать функции DLL, и познакомимся с атрибутами, используемыми для преобразования данных между приложениями .NET и DLL.
Поскольку вы не даете компилятору С# исходный код функции из DLL, вы должны указать ему сигнатуру встроенного метода, информацию о любых возвращаемых значениях, а также способы преобразования параметров для DLL.
ПРИМЕЧАНИЕ
Вы можете создавать DLL с помощью С# и других компиляторов .NET. Я не употребляю термин "неуправляемая Win32 DLL" — такую DLL я называю просто "DLL".
Во-первых, мы рассмотрим объявление на С# простой функции из DLL. Мы используем пример, который для .NET PInvoke быстро становится каноническим, — Win32 "MessageBox". Затем мы перейдем к более сложным вопросам, связанным с преобразованием параметров.
Как вы узнали из главы 8, атрибуты служат для предоставления информации периода разработки о типе С#. В период же выполнения эту информацию позволяет получить отражение. В С# атрибут DllImport позволяет указать компилятору функцию DLL, которую будет вызывать приложение:
модификатор доступа static extern возвр_знанение функциями (парам!, парам2,...);
Как видите, чтобы импортировать функцию из DLL, нужно лишь прикрепить атрибут DllImport (передавая его конструктору имя DLL) к функции DLL, которую вы хотите вызвать. Заметьте: для определяемой вами функции нужно применять модификаторы static и extern. Вот пример MessageBox — он показывает, как легко использовать PInvoke: using System; using System.Runtime.InteropServices;
class PInvokelApp {
[DllImport("user32.dll")] static extern int MessageBoxA(int hWnd,
string msg, string caption, int type);
public static void Main()
{
MessageBoxA(0,
"Hello, World!",
"This is called from a C# app!", 0); } }
В результате запуска этого приложения, как и ожидалось, будет показано окно с сообщением "Hello, World!".
Оператор using указывает пространство имен System.Runtime.InteropServices, которое определяет атрибут DllImport. Затем я определил метод MessageBoxA, вызываемый в Main. Но если вы хотите назвать свой внутренний метод на С# не так, как называется функция DLL? Это позволяет сделать один из именованных параметров атрибута DllImport.
В приведенной ниже программе происходит следующее. Я сообщаю компилятору, что хочу назвать свой метод MessageBoxA. Поскольку в атрибуте DllImport я не указал имя функции DLL, компилятор предполагает, что оба имени одинаковы.
[DllImport("user32.dll")]
static extern int MessageBoxA(int hWnd,
string msg, string caption, int type);
Чтобы понять, как изменить этот стандартный алгоритм, рассмотрим пример. На этот раз используется внутреннее имя MsgBox, но все равно вызывается функция DLL MessageBoxA.
using System;
using System.Runtime.InteropServices;
class PInvoke2App {
[DllImport("user32.dll", EntryPoint="MessageBoxA")]
static extern int MsgBox(int hWnd,
string msg,
string caption,
int type);
public static void Main() {
MsgBox(0,
"Hello, World!",
"This is called from a C# app!", 0}
Как видите, чтобы получить возможность называть внутренний эквивалент внешней функции DLL, как мне вздумается, я должен лишь задать именованный параметр Entry Point атрибута DllImport.
Последнее, что мы рассмотрим, прежде чем перейти к преобразованию параметров, — параметр CharSet. Он позволяет задавать набор символов, используемый файлом DLL. Обычно при написании С++-при-ложений вы не задаете явно использование MessageBoxA или Message-BoxW — благодаря pragma, компилятор уже знает, какой набор символов вы применяете — ANSI или Unicode. Поэтому при вызове MessageBox на C++ компилятор определяет, какой набор символов использовать. Сходным образом на С# вы можете задать целевой набор символов для атрибута DllImport, который будет указывать, какую версию MessageBox вызвать. В следующем примере все равно будет вызвана функция MessageBoxA, так как DllImport через его именованный параметр CharSet передается соответствующее значение.
using System;
using System.Runtime.InteropServices;
class PInvokeSApp {
// При задании CharSet.Ansi будет вызвана MessageBoxA.
// При задании CharSet.Unicode будет вызвана HessageBoxW.
[DllImport("user32.dll", CharSet=CharSet.Ansi)]
static extern int MessageBox(int hWnd, string msg, string caption, int type);
public static void Main() {
MessageBox(0,
"Hello, World!",
"This is called from a C# app!", 0) } }
Преимущество параметра CharSet в том, что я могу установить в приложении переменную, которая будет контролировать вызов той или иной версии функций (ANSI или Unicode). Мне не придется менять весь код при переходе от одной версии к другой.
Не только функции DLL могут быть вызваны приложениями на С#, но и сами функции DLL могут также вызывать определенные методы С# из вашего приложения в сценариях с обратными вызовами. Сценарии обратного вызова включают в себя использование любой из функций Win32 ЕпитХХХ. При вызове этой функции для перечисления элементов ей передается указатель на функцию, которая будет активизироваться Windows каждый раз, когда искомый элемент будет найден. Это делается комбинированием PInvoke (для вызова функции DLL) и делегатов (для определения обратного вызова). О делегатах см. главу 14.
Этот код выполняет перечисление и вывод заголовков всех окон в системе:
using System;
using System.Runtime.InteropServices;
using System.Text;
class CallbackApp {
[DllImport("user32.dll")]
static extern int GetWindowText(int hWnd, StringBuilder text, int
count);
delegate bool CallbackDef(int hWnd, int IParam);
[DllImport("user32.dll")]
static extern int EnumWindows (CallbackDef callback, int IParam);
static bool PrintWindow(int hWnd, int IParam)
{
StringBuilder text = new StringBuilder(255);
GetWindowText(hWnd, text, 255);
Console.WriteLine("Window caption: {0}", text);
return true; }
static void Main() {
CallbackDef callback = new CallbackDef(PrintWindow);
EnumWindows(callback, 0); } }
Сначала я определяю \Мп32-функции EnumWindows и GetWindowText с помощью атрибута DllImport. Затем я определяю делегат CallbackDef 'и метод PrintWindows. После этого мне остается только в методе Main создать экземпляр делегата CallbackDef (передавая ему метод PrintWindows) и вызвать метод Епит Windows. Для каждого окна, найденного в системе, Windows вызовет метод PrintWindows.
Метод PrintWindows интересен, так как использует класс StringBuilder для создания строки фиксированной длины, которую он передает функции GetWindowText. Поэтому функция GetWindowText определяется так:
static extern int GetWindowText(int hWnd, StringBuilder text, int count);
Причина в том, что функции DLL не разрешается изменять строку, так что вы не можете использовать этот тип. И если даже попытаться осуществить передачу по ссылке, вызывающий код не может инициализировать строку с правильным размером. Тут в дело вступает класс String-Builder. Коль скоро длина текста не превышает максимального значения, переданного конструктору StringBuilder, объект StringBuilder может быть разыменован и модифицирован вызванной функцией.
Хотя вы, как правило, не замечаете преобразования и не определяете его работу, каждый раз при вызове функции DLL .NET должен преобразовать параметры для этой функции и возвращаемое значение — для вызывающего приложения .NET. В предыдущих примерах этой главы мне ничего не приходилось делать для этого, так как для каждого типа в .NET определен встроенный тип по умолчанию. Например, тип второго и третьего параметров функций MessageBoxA и MessageBox допределен как строковый. Однако компилятору С# известно, что эквивалентом строки С# является Win32 LPSTR. А если вам захочется изменить поведение при преобразовании, установленное .NET по умолчанию? Для этого служит атрибут MarshallAs, который также определен в пространстве имен System. Runtime. InteropServices.
В следующем примере я снова применю MessageBox во избежание излишнего усложнения. Здесь я выбрал Unicode-версию Щп32-функции
MessageBox. Как вам уже известно, мне требуется задать перечисление CharSet. Unicode для именованного параметра CharSet атрибута DllImport. Однако в этом случае я хочу, чтобы компилятор преобразовывал данные в "широкие" символы (LPWSTR), поэтому я использую атрибут Marshal-As и задаю с перечислением UnmanagedType тип, в который я хочу конвертировать мой собственный тип. Вот этот код:
using System;
using System.Runtime.InteropServices;
class PInvoke4App {
[DllImport("user32.dll", CharSet=CharSet.Unicode)]
static extern int MessageBox(int hWnd,
[MarshalAs(UnmanagedType.LPWStr)]
string msg,
[MarshalAs(UnmanagedType.LPWStr)]
string caption,
int type);
public static void Main() <
MessageBox(0,
"Hello, World!",
"This is called from a C# app!", 0); } }
Атрибут MarshalAs может быть прикреплен к параметрам метода (как в этом примере), возвращаемым значениям метода и полям структур или классов. Заметьте также: чтобы изменить преобразование по умолчанию возвращаемого значения метода, прикрепите атрибут MarshalAs к самому методу.
При переходе с C++ на С# некоторые беспокоятся, останется ли у них "полный контроль" над памятью, когда это необходимо, — эта пробле-'ма связана с небезопасным кодом (unsafe code). Хотя название звучит угрожающе, опасность и ненадежность вовсе не являются его врожденными чертами. Это код, выделение и освобождение памяти для которого не контролируется исполняющей средой .NET. Небезопасный код дает особенно заметные преимущества при использовании указателей для взаимодействия с унаследованным кодом (таким как API для С) или когда вашему приложению требуется прямое манипулирование памятью (как правило, из соображений повышения производительности).
Вы можете писать небезопасный код, применяя ключевые слова unsafe и fixed. Первое указывает, что помеченный им блок будет работать в неуправляемом контексте. Его можно применять ко всем методам, включая конструкторы и свойства, и даже к блокам кода внутри методов. Второе отвечает за фиксацию (pinning) управляемых объектов. Фиксация налагает запрет на перемещение данного объекта сборщиком мусора (GC). По мере исполнения приложения выделенная под объекты память освобождается, в итоге остаются "фрагменты" свободной памяти. Чтобы не допустить фрагментации памяти, исполняющая среда .NET перемещает объекты, обеспечивая максимально эффективное использование памяти. Что хорошего в том, что при наличии указателя на конкретный адрес в памяти, .NET сдвинет находившийся по этому адресу объект без вашего ведома, оставив вас с неверным указателем! Поскольку GC перемещает объекты в памяти, чтобы увеличить эффективность работы приложения, будьте благоразумны, используя fixed.
Рассмотрим некоторые правила, которые относятся к использованию указателей и небезопасного кода на С#. Затем мы погрузимся в изучение примеров. Указатели можно получить только на размерные типы, массивы и строки. В случае массивов их первый элемент должен быть размерного типа, так как С# возвращается указатель на первый элемент массива, а не на сам массив. Поэтому, с точки зрения компилятора, при этом все равно возвращается указатель на размерный тип, а не на ссылочный.
Поддержка стандартной семантики указателей C/C++ в С# проиллюстрирована ниже (табл. 17-1).
Табл. 17-1. Операторы для работы с указателями C/C++.
Оператор
|
Описание
|
&
|
Оператор адрес (address-of) возвращает указатель, представляющий
|
*
|
Оператор разыменования (dereference) обозначает значение
|
->
|
Оператор разыменования и доступа к члену (dereferencing and member access) используется для доступа к члену и разыменования указателя.
|
Следующий пример покажется знакомым любому разработчику на С или C++. В нем я вызываю метод, принимающий два указателя на переменные типа int и изменяющий их значения перед возвратом управления вызывающей функции. Не сильно впечатляющая, но действительно наглядная иллюстрация применения указателей на С#.
// Компилируйте это приложение с параметром /unsafe using System;
class UnsafelApp {
public static unsafe void GetValues(int* x, int* y)
{
*x = 6;
*y = 42; }
public static unsafe void Main()
{
int a = 1; int b = 2; Console.WriteLine("Before GetValuesO : a = {0>, b = {1}",
a, b);
GetValues(&a, &b); Console.WriteLine("After GetValuesO : a = {0}, b = {1}",
a, b); } }
Этот пример необходимо скомпилировать с параметром компилятора /unsafe. Полученное приложение должно выдать:
Before GetValuesO : a = 1, b = 2 After GetValuesO : a = 6, b = 42
Синтаксис оператора fixed таков:
fixed (тип* указатель = выражение) оператор
Как сказано выше, этот оператор говорит GC, чтобы тот не трогал заданную переменную. Заметьте: тип — это неуправляемый тип или void,
выражение — любое выражение, результатом которого является указатель на тип, а словом оператор обозначен блок кода, к которому применима фиксация переменной. Вот простой пример.
using System;
class Foo {
public int x; }
class FixedlApp {
unsafe static void SetFooValue(int* x)
{
Console.WriteLineC'Dereferenced pointer to modify foo.x"); *x = 42; }
unsafe static void Main() {
// Создаем экземпляр структуры.
Console.WriteLine("Creating the Foo class");
Foo foo = new Foo();
Console.WriteLineC'foo.x intialized to {0}", foo.x);
// Оператор fixed фиксирует объект foo в пределах
// включающего его составного оператора. Console.Writel_ine("Setting pointer to foo.x");
// Присвоить Foo* адрес объекта foo. fixed(int* f = &foo.x) <
Console.WriteLine("Calling SetFooValue passing " + "pointer to foo.x");
SetFooValue(f); >
// Убедимся, что мы действительно изменили член через
// указатель на него.
Console.WriteLine("After return from " +
"SetFooValue, foo.x = {0}", foo.x); } >
Этот код создает экземпляр класса Foo и фиксирует его оператором fixed, присваивая адрес его первого члена переменной типа int* (этот тип требуется методу SetFooValue). Заметьте, что оператор fixed включает именно тот код, на котором отразятся изменения при сдвиге объекта Foo сборщиком мусора. Это тонкий, но важный вопрос, касающийся больших и дольше исполняемых блоков кода, для которых вы хотите свести к минимуму время, в течение которого объект будет зафиксирован. В результате компиляции и запуска этого кода вы получите:
Creating the Foo class foo.x intialized to 0
Setting pointer to foo.x Calling SetFooValue passing pointer to foo.x
Dereferenced pointer to modify foo.x After return from SetFooValue, foo.x = 42
ПРИМЕЧАНИЕ
Компилятор С# не ограничивает доступ к фиксированной переменной областью видимости небезопасного кода. Например, вы можете использовать фиксированную переменную как г- value для /-value, определенному в более широкой области видимости, чем область видимости небезопасного блока. В результате может получиться небезопасное значение, применяемое вне небезопасного блока. В этом случае компилятор не выдает предупреждений, а ответственность за соблюдение осторожности при использовании фиксированных переменных в качестве r-value ложится на разработчика.
Вас, конечно, интересует, как те СОМ-компоненты, что вы написали за долгие годы, работают с .NET. Что ж, я покажу, как компоненты классической СОМ (да-а-а... больно слышать, что СОМ уже называют классической) позиционируются в мире .NET.
Как вы могли убедиться, среда .NET и язык С# вместе представляют собой мощные средства создания компонентных систем. Но как быть с тоннами СОМ-компонентов многократного использования, написанных вами за последние годы, не считая всех этих чашек кофе и бессонных ночей? Не звучит ли .NET как приговор этим компонентам? Будут ли они работать "рука об руку" с управляемой средой выполнения .NET? Для тех из нас, кто жить не может без программирования с СОМ и живущих с мантрой "СОМ — это хорошо", есть отличные новости. СОМ никуда не денется, а приложения в среде .NET смогут использовать существующие компоненты СОМ. Как вы уже догадываетесь, компоненты классической СОМ взаимодействуют с исполняющей средой .NET через слой интероперабельности (COM Interop), который передает все сообщения между управляемой средой выполнения .NET и компонентами СОМ, функционирующими в неуправляемом мире.
Поскольку слой взаимодействия с СОМ поначалу может несколько ошеломить, давайте на минуту забудем обо всех формальных определениях j и перейдем к реальному примеру, иллюстрирующему применение ком- / понента СОМ из приложения .NET. По мере продвижения вперед я буду/ давать разъяснение происходящих событий и расскажу, как использовать приобретенные знания в ваших приложениях.
В этом примере мы предположим, что у нас есть СОМ-компонент Airlinelnfo, написанный на Microsoft Visual C++ с ATL. Я не будут освещать здесь все этапы создания этого компонента, потому что хочу сосредоточиться на вопросах, связанных с .NET и С#. Однако я разъясню этот выдающийся код (полный проект Visual C++ находится на CD, прилагаемом к этой книге).
Наш компонент СОМ разработан для того, чтобы выдавать подробности о прибытии рейсов некоторой авиалинии. Для простоты допустим, что компонент возвращает данные для авиалинии Air Scooby 1C 5678, а в случае любой другой авиалинии — ошибку. Я намеренно вставил механизм генерации ошибки, чтобы показать вам, как ошибка, возникшая в компоненте СОМ, может проходить в обратном направлении и перехватываться вызывающим клиентским приложением .NET. Вот IDL для компонента СОМ:
interface lAirlinelnfo : IDispatch {
[id(1), helpstring("method GetAirlineTiming")]
HRESULT GetAirlineTiming([in] BSTR bstrAirline, [out,retval] BSTR* pBstrDetails);
[propget, id(2), helpstring("property LocalTiraeAtOrlando")]
HRESULT LocalTimeAtOrlando([out, retval] BSTR •pVal); };
Ничего особенного, даже для самых неопытных разработчиков СОМ. У нас есть интерфейс с именем lAirlinelnfo и двумя методами: GetAirlineTiming и LocalTimeAtOrlando. А теперь взгляните на настоящую реализацию метода GetAirlineTiming:
STDMETHODIMP CAirlineInfo::GetAirlineTiming(BSTR bstrAirline, BSTR *pBstrDetails)
{
\ _bstr_t bstrQueryAirline(bstrAirline);
\ if(NULL == pBstrDetails) return E.POINTER;
\ if(_bstr_t("Air Scooby 1C 5678") ==
\ bstrdueryAirline)
\ {
I // Вернуть расписание для этой авиалинии.
\ "pBstrDetails =
| _bstr_t(_T("16:45:00 - Will
arrive at Terminal 3")).copy();
}
else
{
II Вернуть сообщение об ошибке.
return Error(LPCTSTR(_T("Not available" )),
__uuidof(Airlinelnfo),
AIRLINE_NOT_FOUND);
return S_OK; }
Метод GetAirlineTiming принимает два аргумента. Первый (bstrAirline) — это строка BSTR, представляющая авиалинию, а второй (pBstrDetails) — выходной параметр, возвращающий информацию о прибытии (местное время и проход для регистрации). В этом методе мы проверяем, равно ли значение входного параметра bstrAirline строке "Air Scooby 1C 5678". Если это так, то мы возвращаем некоторую жестко заданную информацию о прибытии. Если значение не совпадает с ожидаемым, мы вызываем метод ошибки, чтобы вернуть уведомление о том, что поддерживается только одна авиалиния.
Закончив с обзором основ использования компонентов, рассмотрим генерирование метаданных из библиотеки типов компонента (typelib), которые .NET-клиент может использовать для "переговоров" с нашим компонентом и вызова его методов.
Приложение .NET, которому требуется взаимодействие с нашим компонентом СОМ, не может непосредственно воспользоваться функциональностью этого компонента. Почему нет? Как нам известно из главы 16, .NET создана для работы с компонентами, у которых есть метаданные, тогда как СОМ работает через реестр и с помощью набора методов опроса, реализованных компонентом. Поэтому первое, что нам надо сделать, чтобы этот компонент СОМ применялся в .NET, — это сгенерировать для него метаданные. В случае компонента СОМ этот слой метаданных позволяет исполняющей среде определить информацию о типе. / Далее эта информация о типе используется в период выполнения для / создания сущности под названием оболочка, обеспечивающая обращение! в период выполнения (runtime callable wrapper, RCW — рис. 17-1). RCW реально активизирует объект СОМ и преобразует данные при взаимодеш ствии с приложением .NET. RCW также выполняет уйму другой работы: управление идентификационными данными и сроками жизни объекте^, а также кэширование интерфейсов. ,
Рис .17.1. Базовые компоненты взаимодействия .NET-СОМ.
Управление сроком жизни объектов — проблема важнейшая, так как .NET GC перемещает объекты и автоматически избавляется от них, когда они больше не используются. RCW создает для приложения .NET иллюзию того, что оно взаимодействует с управляемым компонентом .NET, а для компонента СОМ в неуправляемом пространстве она формирует впечатление, что его вызывает традиционный клиент СОМ. Создание и поведение RCW варьирует в зависимости от того, каким является связывание с объектом СОМ: ранним или поздним. Внутренние механизмы RCW выполняют всю черную работу и шлюзование вызовов всех методов в соответствующие vtable-вызовы компонента СОМ, живущего в неуправляемом мире. В основном она действует как посол доброй воли между управляемым миром и неуправляемым миром lUnknown. Но хватит болтать! Сгенерируем оболочку метаданных для нашего компонента COM Airlinelnfo. Для этого нам потребуется утилита Туре \ Library Importer (tlbimp.exe). Эта утилита, поставляемая с .NET SDK, слу-\ жит для считывания библиотеки типов СОМ и генерации соответствующей оболочки метаданных, содержащей информацию о типе, понятную исполняющей среде .NET. Для этого нам нужно установить демо-приложения с CD, прилагаемого к книге, и найти компонент Airlinelnfo. ' Сделав это, наберите в командной строке:
HBIMP Airlinelnformation.tlb /out:AirlineMetadata.dll
Эта команда заставляет TLBIMP прочитать библиотеку типов COM Airlinelnfo и сгенерировать соответствующую оболочку метаданных с именем AirlineMetadata.dll. Если все будет работать, как надо, вы увидите сообщение:
TypeLib imported successfully to AirlineMetadata.dll
Какую информацию о типах содержат эти метаданные и как она выглядит? Как и все работающие с СОМ, вы молились на нашу любимую утилиту OleView.exe — ведь она, помимо прочего, позволяет нам изучать содержимое typelibl К счастью, в поставку .NET SDK входит нечто похожее — дизассемблер IL под названием ILDASM, с которым вы познакомились в главе 2. Он позволяет просматривать метаданные и код MSIL, сгенерированный для управляемых сборок. Как вы узнали из главы 16, каждая управляемая сборка содержит самоописывающиеся метаданные, a ILDASM — очень полезный инструмент для просмотра этих метаданных. Итак, откроем AirlineMetadata.dll с помощью ILDASM (рис. 17-2).
Просматривая сгенерированные метаданные, вы можете заметить, что метод GetAirlineTiming указан как открытый член класса Airlinelnfo. Там присутствует и конструктор этого класса. Заметьте, что параметры метода были автоматически замещены своими эквивалентами в .NET. В этом примере BSTR был заменен параметром System.String. Обратите внимание и на то, что параметр с пометкой [out,retval] метода GetAirlineTiming был преобразован в реальное значение, возвращаемое методом (в виде System.String). Кроме того, любые значения ошибок HRESULT, возвращаемые компонентом СОМ в случае ошибки или сбоя прикладной логики, ведут к генерации исключений.
Рис. 17.2. ILDASM - замечательный инструмент просмотра метаданных и MSIL управляемых сборок.
Теперь, сгенерировав метаданные для .NET-клиента, попытаемся вызвать метод GetAirlineTiming нашего объекта СОМ из .NET-клиента. Вот клиентское приложение, написанное на С#, которое создает объект СОМ с помощью сгенерированных ранее метаданных и вызывает метод GetAirlineTiming. В этом примере мы используем раннее связывание. Я покажу вам еще два коротких примера, иллюстрирующих динамическое определение типов и позднее связывание.
using System;
using System.Runtime.InteropServices;
using System.Reflection;
using AIRLINEINFORMATIONLib;
public class AirlineClientlApp
{
public static void Main() <
/// ПРИМЕР РАННЕГО СВЯЗЫВАНИЯ ///
/// String strAirline = "Air Scooby 1C 5678";
///String strFoodJunkieAirline = "Air Jughead TX 1234"; | try
i Airiinelnfo objAirlinelnfo;
// Создать новый объект Airiinelnfo. objAirlinelnfo = new AirlinelnfoO;
// Отобразить выходную информацию после вызова
// метода GetAirileTiming.
Console.WriteLine("Details for Airline {0} -> {1}",
strAirline,objAirlinelnfo.GetAirlineTiming(strAirline));
// Ошибка: в результате выполнения следующего кода
// будет сгенерировано исключение!
// Console.WriteLineC'Details for Airline {0} -> {1}",
// strFoodJunkieAirline,objAirlinelnfo.GetAirlineTiming
// (strFoodJunkieAirline)); >
catch(COMException e) {
Console.WriteLine("Oops- We encountered an error " +
"for Airline {0}. The Error message " + "is : {1}.
The Error code is {2}", StrFoodJunkieAirline , e.Message,e.ErrorCode); } } }
Все происходящие здесь события — результат того, что исполняющая среда создает RCW, которая увязывает методы и поля классов метаданных с методами и свойствами, предоставленные интерфейсами, которые реализованы объектом СОМ. Для каждого объекта СОМ создается по одному экземпляру RCW. Исполняющая среда .NET занимается только управлением сроком жизни RCW, а сборщик мусора утилизирует отслужившую свое оболочку. Именно RCW заботится о поддержке счетчиков ссылок на объект СОМ, которому она соответствует. Таким образом, RCW избавляет исполняющую среду .NET от управления счетчиками ссылок на реальный объект СОМ. Метаданные Airlinelnfo определены в пространстве имен AIRLINEINFORMATIONLib (рис. 17-2). .NET-клиент воспринимает все методы интерфейса так, как если бы они были членами класса Airlinelnfo. Все, что нам нужно сделать, — это создать эк- / земпляр класса Airlinelnfo с помощью оператора new и вызвать открытые f методы класса созданного объекта. При вызове метода RCW осуществ- ) ляет шлюзование этого вызова в вызов соответствующего метода СОМ./ RCW также решает все вопросы, связанные с преобразованием и сроком/ жизни объекта. .NET-клиент воспринимает это просто как создание тиг пичного управляемого объекта и вызов одного из его открытых членов! Каждый раз, когда метод СОМ генерирует ошибку, ошибка СОМ перехватывается RCW. Далее эта ошибка преобразуется в эквивалентный класс COMException (он находится в пространстве имен System.Runtime.In-teropServices). Конечно же, СОМ-объект все равно должен реализовывать интерфейс ISupportErrorlnfo, чтобы передавать ошибки. Это позволяет RCW узнать, что ваш объект обеспечивает расширенную информацию об ошибках. Ошибку может перехватить .NET-клиент, используя обычный механизм обработки исключений try-catch, а у клиента есть доступ к номеру ошибки, описанию и источнику исключения, а также к другим деталям, доступным любому клиенту, поддерживающему СОМ. А теперь продолжим изучение этого примера и рассмотрим другие средства связывания с компонентами СОМ.
Как же работает классический сценарий Query Interface с точки зрения .NET-клиента, которому нужен доступ к другому интерфейсу, реализованному объектом СОМ? Все, что вам нужно, чтобы выполнить запрос QI для другого интерфейса, — это привести текущий объект к другому нужному вам интерфейсу, и — пожалуйста! — ваш запрос QI выполнен! Теперь вы готовы к вызову всех методов и свойств нужного интерфейса. Это просто.
Вся черновая закулисная работа снова достается RCW. В этом смысле она аналогична исполняющей среде Visual Basic, которая защищает разработчиков клиентов СОМ от написания любого явного кода, связанного с Query Interface. Она просто выполняет для вас запросы QI, когда вы устанавливаете объект одного типа как объект другого ассоциированного типа.
Посмотрим на этот механизм в действии, чтобы узнать, как легко его использовать. Имея в виду наш пример, предположим, что вы хотели вызвать методы интерфейса lAirportFacilities, которые для нашего объекта СОМ реализованы другим интерфейсом. Для этого вам будет нужно привести объект Airlinelnfo к интерфейсу lAirportFacilities. Теперь вы можете вызывать все методы, которые являются частью интерфейса lAirportFacilities. Но перед приведением вы, возможно, захотите проверить, поддерживает или реализует ли имеющийся в данный момент экземпляр объекта тип интерфейса, который мы запрашиваем. Это можно сделать, вызвав метод IsInstanceOf класса System. Type. Если он возвращает true, вы знаете, что QI выполнен успешно и можно выполнить приведение. В случае приведения объекта к некоторому интерфейсу, не поддерживаемому этим объектом, генерируется исключение System.InvalidCastExcep-tion. Таким образом, RCW гарантирует, что выполняется приведение только к интерфейсам, реализованным объектом СОМ. Вот как это выглядит в написанном коде:
using System;
using System.Runtime.InteropServices;
using System.Reflection;
using AIRLINEINFORMATIONLib;
public class AirlineClient2App {
public static void Main() {
///////////////////////////////////////////////
/// Запрос интерфейса/проверка типа в период выполнения.
///////////////////////////////////////////////
try
{
Airlinelnfo objAirlinelnfo; lAirportFacilitiesInfo objFacilitiesInfo;
// Создать новый объект Airlinelnfo. objAirlinelnfo = new AirlinelnfoO;
// Вызвать метод GetAirlineTiming. String strDetails = objAirlinelnfo.GetAirlineTiming
(strAirline);
// Запросить интерфейс lAirportFacilitiesInfo. objFacilitiesInfo .=
(lAirportFacilitiesInfo)objAirlinelnfo;
//Вызвать метод интерфейса lAirportFacilitiesInfo Console.WriteLine("{0}",
objFacilitiesInfo.GetlnternetCafeLocationsO);
{
catch(InvalidCastException eCast)
}
Console.WriteLine("We got an InvalidCast Exception " +
"- Message is {0}",eCast.Message); }}}
Оба приложения — AirlineClientlApp и AirlineClient2App — используют метаданные RCW для раннего связывания .NET-клиента с объектом СОМ. Хотя раннее связывание обеспечивает строгую проверку типов в период компиляции, возможность вывода подсказок о типах в средах разработки (например, Visual Studio.NET) и, конечно, лучшую производительность, могут быть случаи, когда у вас нет метаданных периода компиляции для объекта СОМ, с которым вы осуществляете связывание. И тогда вам требуется позднее (динамическое) связывание (late binding) с этим компонентом. Например, если компонент, который вы пытаетесь задействовать, содержит только диспинтерфейс, то при использовании этого компонента вы во многом ограничены поздним связыванием.
Вы можете производить позднее связывание с СОМ-объектом через механизм отражения (см. главу 16). Для этого вам нужно знать ProgID компонента, так как статический метод Create Instance класса System.Acti-vator требует объект Type. ProgID компонента позволяет вызвать метод GetTypeFromProgID класса System. Type. Он вернет допустимый объект Туре .NET, который вы в дальнейшем сможете использовать при вызове метода System.Activator.Createlnstance. Сделав это с помощью метода экземпляра System. Type.InvokeMember объекта Туре, полученного от метода GetTypeFromProgID, вы сможете вызывать любые методы или свойства, которые поддерживает интерфейс по умолчанию компонента.
Все что вам нужно знать, — это имя метода или свойства и сведения о параметрах, принимаемые при вызове метода. При вызове метода поздно связанного компонента вы можете передать параметры, объединив их в универсальный массив System.Object, после чего последний передается методу. Вам также потребуется установить флаги связывания (binding flags) в зависимости от того, что вы делаете: вызываете метод или получаете/устанавливаете значение свойства.
Как видно из следующего кода, здесь придется поработать чуть больше, чем при раннем связывании. Однако вы будете очень рады и этому, если позднее связывание — единственный выбор.
using System;
using System.Runtime.InteropServices;
using System.Reflection;
using AIRLINEINFORMATIONLib;
public class AirlineClientSApp {
public static void Main() {
///////////////////////////////////////////////
/// ПОЗДНЕЕ СВЯЗЫВАНИЕ
///////////////////////////////////////////////
try
{
object objAirlineLateBound;
Type objTypeAirline;
object[] array!nputParams= { "Air Scooby 1C 5678" };
objTypeAirline = Type.GetTypeFromProgID
("Airlinelnformation.Airlinelnfo");
ObjAirlineLateBound = Activator.Createlnstance
(objTypeAirline);
String str = (String)objTypeAirline.InvokeMember
("GetAirlineTiming", BindingFlags.Default | BindingFlags.InvokeMethod, null, objAirlineLateBound, arraylnputParams);
Console.WriteLine("{0}", str);
String strTime = (String)objTypeAirline.InvokeMember
("LocalTimeAtOrlando", BindingFlags.Default | BindingFlags.GetProperty, null, objAirlineLateBound, new object [] {});
Console.WriteLine ("Hi there !. The Local Time in " +
"Orlando,Florida is: {0}", strTime); }
catch(COMException e) {
Console.Writel_ine("0ops- We encountered an error " +
"for Airline {0}. The Error message " + "is : {1}. The Error code is {2}",
strFoodJunkieAirline, e.Message,e.ErrorCode); > } }
В начале программирования в СОМ большинство людей мало знает (или не знают вообще) о моделях потоков и окружений в СОМ. Не набравшись опыта, они не понимают, что за модель свободных потоков приходится расплачиваться значительным снижением производительности, когда клиентский поток в однопоточном окружении (single-threaded apartment, STA) используется для создания объекта в многопоточном окружении (multithreaded apartment, MTA). Кроме того, начинающие программисты в СОМ часто не знают о безопасности потоков и о том, чем им угрожает одновременное обращение нескольких потоков к их компонентам СОМ.
Прежде чем вызвать объект СОМ, поток должен объявить свою принадлежность к определенному окружению, указывая, что он может входить в STA или МТА. Клиентские потоки STA вызывают Colnitiali-ze(NULL) или Со Initialize Ex(О, COINIT_APARTMENTTHREADED), чтобы войти в STA, а потоки МТА вызывают Со Initialize Ех(О, COINIT_MULTI-THREADED) для входа в МТА. Сходным образом в мире .NET у вас есть выбор: вы можете позволить вызывающему потоку в управляемом пространстве объявить свою принадлежность к некоторому окружению. Вызывающий поток в управляемом приложении выбирает МТА как свое место обитания по умолчанию. Это аналогично тому, как если бы вызывающий поток инициализировался с параметрами CoInitializeEx(0, COINIT_MULTITHREADED). Но подумайте об издержках и потерях производительности, которые будут иметь место, если вызывающим потоком будет классический СОМ-компонент STA, разработанный для отдельного потока многопоточного процесса. Несовместимые окружения также приведут к издержкам в связи с дополнительными парами "про-кси/заглушка", за что определенно придется расплачиваться производительностью.
В связи с этим вы можете изменить выбор окружения по умолчанию для управляемого потока в приложении .NET с помощью свойства Apart-mentState класса System. Threading. Thread. Свойство ApartmentState принимает одно из значений:
- МТА — многопоточное окружение;
- STA — однопоточное окружение;
- Unknown — эквивалентно поведению МТА по умолчанию.
До осуществления любых вызовов объекта СОМ вам также нужно задать свойство ApartmentState для вызывающего потока. Заметьте: после создания объекта СОМ изменить свойство ApartmentState нельзя. Поэтому имеет смысл установить свойство потока ApartmentState в программе как можно раньше. Вот как это сделать:
// Установить свойство клиентского потока ApartmentState для входа в STA.
Thread.CurrentThread.ApartmentState = ApartmentState.STA;
// Создать объект COM посредством Interop. MySTA obj STA = new MySTAQ; objSTA.MyMethodQ
Подведем итоги
Последнее, о чем я хочу сказать, это как различные механизмы работы с унаследованным кодом (PInvoke, небезопасный код и COM Interop) вписываются в общую схему .NET. В этой главе вы узнали следующее. Ш Как пользоваться PInvoke и некоторыми атрибутами, чтобы облегчить решение задач, связанных с преобразованием различных типов данных, включая пользовательские данные, когда применяются стандартные С-подобные вызовы функций.
- Что касается небезопасного кода, вы узнали, как отказываться от преимуществ управляемого кода в приложениях на С#, оказавшись в ситуации, когда вам требуется больший контроль над памятью. Эти сценарии могут включать случаи, когда вам требуется ручное манипулирование памятью ради повышения эффективности или при перемещении в приложение на С# блоков кода, к преобразованию которого в управляемый код вы просто еще не готовы.
- Относительно СОМ вы увидели, как задействовать классические компоненты СОМ в приложениях .NET и как COM Interop обеспечивает бесшовное многократное использование существующих компонентов СОМ из управляемого кода. Далее мы бегло ознакомились со способами вызова вашего компонента СОМ с помощью как раннего, так и позднего связывания, а также со способами проверки типов в период выполнения. В завершение вы увидели, как управляемые потоки объявляют о своей принадлежности к тому или иному окружению при вызове компонентов СОМ.
Теперь у вас, как у разработчиков, использующих один из этих механизмов для работы с неуправляемым кодом, может возникнуть вопрос. Продолжать ли применять эти методологии или сразу перейти в мир .NET путем написания всех компонентов и кода прикладной логики в виде управляемых компонентов с помощью такого языка .NET, как С#? Ответ зависит от конкретных обстоятельств.
Если у вас тонны унаследованного кода, будь это С-подобные функции в DLL, код, непосредственно манипулирующий памятью, компоненты СОМ или комбинация всех трех видов кода, за одну ночь вы, по всей вероятности, не сможете конвертировать все это богатство. Тогда имеет смысл задействовать различные механизмы .NET для работы с унаследованным кодом. Если же вы пишете код прикладной логики "с нуля", я бы искренне советовал вам писать его в виде управляемых компонентов на таком языке, как С#. При этом вы покончите с потерями производительности, неизбежно возникающими при переходе границ между управляемым и неуправляемым мирами.