Продолжается подписка на наши издания! Вы не забыли подписаться?

Переход на .Net: взаимодействие старого и нового**

М. Шиманович
Часто встречающийся в тексте значок <...> заменяет жизненно важную именно для Вас информацию, из-за нашей жадности отсутствующую в Web-версии статьи. Однако в печатной версии вы вполне можете её найти.

Большинство программистов, узнав о преимуществах и достоинствах .Net, буквально подавлены радикальностью изменений. Это выражается во фразах типа: «Я ничего против не имею, просто очень жалко вложений в С++ - как-никак большая часть жизни…» А ведь .Net вторгается на территорию не только языков 3 поколения типа С++ и Object Pascal, но и в другие области разработки ПО. Хотя в последнее время Microsoft и не делает на этом акцента, но .Net должен заменить собой и COM, и COM+. А раньше об этом говорилось четко и ясно. Желающие убедиться в этом, могут почитать статьи, публиковавшиеся в MSJ, переименованном в MSDN Magazine, по адресу http://www.microsoft.com/msj/defaultframe.asp?page=/msj/1197/complus.htm  и http://www.microsoft.com/msj/defaultframe.asp?page= /msj/1297/complus2/complus2.htm. Там рассказывается о замечательной технологии COM+, но не той, знакомой всем и вошедшей в W2K, а другой, больше похожей на гибрид новой версии COM и специально созданного для нее языка программирования. Нет, не подумайте, кроме идей атрибутного программирования, в этой статье временами проскакивали идеи, впоследствии воплотившиеся в жизнь в W2k. Но эти идеи – всего лишь малая часть того, что в результате воплотилось в жизнь под именем .Net. Вместо улучшения COM возник совершенно новый продукт, который, оставив основные концепции, так дополнил и развил их, что камня на камне не оставил. В общем, никто не ушел обиженным.

Дон Бокс, один из лучших авторов на тему COM, даже заявил, что COM, Win32 API и С++-компилятор от Microsoft мертвы (www.zdnet.com/zdnn/stories/news/0,4586,27111 22,00.html) и не будут развиваться. Там же прозвучало, что .Net для Microsoft – это новая ОС, а NT отводится роль Hardware Abstraction Layer.

Но слухи о смерти COM сильно преувеличены. За всей суетой вокруг .NET Framework пресса как-то забывает, что никакого .Net пока нет (простите за невольный каламбур), и еще минимум квартал не будет. А когда оно появится, то будет в положении цветного телевизора 40 лет назад, Windows 3.0 11 лет назад или DVD-ROM сейчас. Оно круче, оно лучше, в нем больше калорий на килограмм живого веса, но народ вложил уже кучу денег, сил и времени в другие стандарты и не может выкинуть все это на ветер просто так. Мы увязли в существующих технологиях, как Microsoft в судебных исках. Платформы Микрософт основывались на COM в течение последних 8 лет, что в нашем бизнесе почти равно вечности. Первое .Net-приложение обнаружит вокруг себя несметную прорву COM-приложений и, по определению, ни одного .Net-приложения. Второе .Net-приложение обнаружит вокруг себя толпы COM-приложений и одно .Net-приложение, ну, и так далее. Учитывая очевидный дефицит талантливых программистов, немногие организации отважатся на переписывание 100% существующего Win32, C, C++, Visual Basic 6.0 и COM-кода, независимо от достоинств новой среды. Нужно обладать капиталами Microsoft, чтобы позволить себе такую роскошь, как переписывание с нуля всех своих продуктов.

Точно так же, как первые цветные телевизоры в основном показывали черно-белые программы, а Windows 3.0 служила оболочкой для запуска DOS-приложений, иначе ее никто не купил бы, так и .Net должна (или должно?) гладко взаимодействовать с COM. Иначе в перспективность такого проекта не поверил бы даже Билл Гейтс. И оно это делает, причем и как .Net-клиент, использующий COM-сервер, и наоборот.

Как же реализовано взаимодействие .Net и старого кода? Истоки и первые пробы реализации идей поддержки COM в .Net можно найти в Visual J++. А истоки поддержки API, основанных на DLL, можно найти в VB прошлых версий – 6 и ранее.

Для этого, конечно, нужен некий механизм, позволяющий управляемому коду (managed code), исполняющемуся внутри CLR (MSCOREE.DLL), интегрироваться с работающим вне MSCOREE.DLL обычным кодом. Это значит, что «управляемые программы» должны работать в «неуправляемом (то есть обычном) режиме» достаточно долго для исполнения классического COM или Win32-кода. И ещё, неуправляемым потокам может понадобиться вызвать управляемый метод.

Рисунок 1. Интеграция управляемого и неуправляемого кода

К счастью, CLR позволяет осуществлять такую интеграцию. Типичный пример такого процесса изображен на рисунке 1. Заметьте, что часть кода исполняется внутри MSCOREE.DLL, а часть – нет. Пока сама ОС не будет переписана в управляемом коде, такое положение неизбежно будет сохраняться – так как Windows пока состоит из обычных DLL. Осталось выяснить, насколько такое взаимодействие функционально, удобно и производительно.

Интеграция управляемого и неуправляемого кода

Вызов классической Win32-DLL из CLR достаточно прямолинеен, если использовать P/Invoke (где P означает «platform»). P/Invoke – технология, позволяющая отобразить объявление статического метода на точку входа PE/COFF, разрешаемую через LoadLibrary/GetProcAddress. Как и Java Native Interface (JNI) или J/Direct до него, P/Invoke использует объявление управляемого метода для описания стекового фрейма, но предполагает, что тело метода лежит во внешней, классической DLL. Однако P/Invoke более удобен, поскольку позволяет импортировать любую DLL, а не только специально рассчитанную на CLR.

Чтобы указать, что метод определен во внешней DLL, нужно пометить его как extern и использовать атрибут метода System.Runtime.InteropServices.DllImport. Этот атрибут сообщает CLR, что описание метода и дополнительные параметры (если они есть) необходимо использовать как информацию для вызова LoadLibrary и GetProcAddress, перед тем, как вызвать метод.

<...>

Атрибут DllImport имеет ряд параметров. Как показано в Листинге 1, атрибуту DllImport нужно знать по крайней мере имя файла. Это имя используется CLR для вызова LoadLibrary. Имя функции, которую необходимо вызвать из DLL, задается или прямым заданием параметра EntryPoint атрибута DllImport, или берется из описания самой функции. Во втором случае подразумевается, что ее название в программе соответствует ее имени в библиотеке.

Листинг 1. DllImport

namespace System.Runtime.InteropServices
{
  public class DllImportAttribute : Attribute
  {
    public DllImportAttribute(String dllname);
// Атрибут EntryPoint переопределяет имя метода
    public String            EntryPoint;
    // ExactSpelling отключает W/A _@ разыменование
    public bool              ExactSpelling;
    // cdecl, stdcall и т.д.
    public CallingConvention CallingConvention; 
    // unicode/ansi/auto
    public CharSet           CharSet;           
    // функция вызывает SetLastError
    public bool              SetLastError;
    // рассматривает результат HRESULT как [retval]
    public bool              TransformSig;
  }
}

Листинг 2 показывает три варианта описания метода ReadFile из kernel32.dll...

<...>

Конвертация типов

При осуществлении вызова (как в ту, так и в другую сторону) параметры неизменно передаются через стек. Эти параметры одновременно являются экземплярами типов и CLR, и внешнего мира. Ключом к пониманию, как работает interop, служит понимание того, что каждое «значение» имеет два типа – управляемый и неуправляемый. Что важнее, некоторые управляемые типы изоморфны неуправляемым, а это значит, что при необходимости передачи экземпляра такого типа за пределы CLR никакого преобразования не требуется. Однако многие типы не изоморфны, и требуют некоторого приведения в вид, пригодный для внешнего мира.

...При вызове внешней процедуры, принимающей в качестве параметров только изоморфные типы, никакого преобразования не требуется, и как вызывающая, так и вызываемая сторона могут совместно использовать стековый фрейм, хотя одна из них и работает в неуправляемом режиме. Если хотя бы один из параметров будет неизоморфного типа, потребуется маршалинг стекового фрейма в формат, совместимый с миром вне MSCOREE. Такая конверсия может потребоваться и при обратном направлении вызова. К счастью, компилятор C# распознает, в каком направлении передаются значения параметров – по ключевым словам ref и out. Этот эффект показан на рисунке 2.

<...>

С изоморфными типами все просто. С неизоморфными – сложнее. Так, DLL может содержать строки разных типов или структуры, причем поля структуры могут по-разному выравниваться, а некоторые типы данных .Net преобразуются в похожие, но не точно совпадающие типы (см. табл. 1). Ниже мы расскажем о том, как .Net преобразует неизоморфные типы при передаче в неуправляемый мир, и как можно управлять этим преобразованием. Начнем с наиболее часто используемого типа данных – со строк.

Строки, использованные в вызовах P/Invoke

В неуправляемых DLL-библиотеках могут использоваться разные типы строк. Чаще всего мы сталкиваемся со строкой, представляющей из себя массив символов, заканчивающийся нулевым символом. Но даже такой массив символов может храниться в ANSI- или Unicode-формате. В COM-ориентированных API зачастую применяется тип BSTR. Ко всему прочему, строки могут передаваться по указателям и храниться в составе структур. В CLR же работа со строками осуществляется с помощью классов String и StringBuilder. Класс String встроен C# и VB.Net в качестве базового класса. При передаче этих классов в неуправляемый код необходим маршалинг.

Рисунок 2. Неизоморфные параметры

String и StringBuilder в отношении маршалинга похожи. При маршалинге строки могут быть преобразованы в ...

<...>

Контролировать, как производится маршалинг параметра (или поля в структуре), можно с помощью атрибута MarshalAs. Этот атрибут указывает, какой тип должен использоваться за пределами MSCOREE. Атрибут MarshalAs можно использовать для указания соответствующего неуправляемого типа данному параметру или полю. Для многих типов CLR выберет подходящий тип по умолчанию. Однако атрибут MarshalAs позволяет изменить выбираемое по умолчанию значение. Например, код листинга 3 показывает, как с помощью атрибута MarshalAs отобразить CLR-тип System.String на один из четырех общих Win32-форматов:

Листинг 3. MarshalAs с параметрами

using System.Runtime.InteropServices;
public class FooBarWrapper 
{

// Эта процедура оборачивает функцию, объявленную как 
// void _stdcall DoIt(LPCWSTR s1, LPCSTR s2,
//                    LPTSTR  s3, BSTR s4);
[ DllImport(“foobar.dll”) ]
public static extern void DoIt(
    [MarshalAs(UnmanagedType.LPWStr)] String s1,
    [MarshalAs(UnmanagedType.LPStr)] String s2,
    [MarshalAs(UnmanagedType.LPTStr)] String s3,
    [MarshalAs(UnmanagedType.BStr)] String s4
  );
}

Что интересно, все параметры, кроме UnmanagedType.LPWStr, приводят к созданию копии строки, соответствующей нижележащему простому формату. При маршалинге аргумента метода при вызове P/Invoke, опции маршалинга для строк следующие...

<...>

Использование строк в структурах

Строки могут применяться не только в параметрах методов. Структуры тоже могут содержать поля строкового типа. Указание способа маршалинга, как и в случае с параметрами метода, задается атрибутом MarshalAs. Если строка в структуре представлена как указатель, то есть поле описано как LPTSTR, LPSTR, LPWSTR или BSTR, значения этого атрибута ничем не отличаются от описанных выше. Но иногда в структурах строки представлены как массивы символов с фиксированной длиной (inline-строки). .Net не имеет специального типа для описания inline-строк ANSI- или Unicode-символов, но имеет специальный тип для массивов символов, чей тип определяется по CharSet содержащей их структуры.

LPStr, LPWStr, LPTStr и BSTR применяются к ссылкам на строки, содержащиеся внутри структур (структуры, содержащие указатели на строки). Для in-line-массивов символов фиксированной длины, содержащихся внутри структуры, используется ранее не упоминавшийся нами UnmanagedType.ByValTStr. Тип символов, используемый в ByValTStr, определяется аргументом CharSet атрибута StructLayoutAttribute содержащей массив структуры.

Например, следующие структуры содержат ссылки на строки, и строки, определяемые в неуправляемом коде как массивы фиксированной длины, а также ANSI, UNICODE и платформно-зависимые символы.

struct StringInfoA 
{
   char *      f1;
   char        f2[256];
};

struct StringInfoW 
{
   WCHAR *   f1;
   WCHAR     f2[256];
   BSTR      f3;
};

struct StringInfoT 
{
   TCHAR *   f1;
   TCHAR     f2[256];
};

Следующие определения управляемых типов показывают, как можно использовать атрибут MarshalAs для определения таких же структур в C#.

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi)]
struct StringInfoA 
{
    [MarshalAs(UnmanagedType.LPStr)] public String f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public String f2;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
struct StringInfoW 
{
    [MarshalAs(UnmanagedType.LPWStr)] public String f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public String f2;
    [MarshalAs(UnmanagedType.BStr)] public String f3;
}

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]
struct StringInfoT 
{
    [MarshalAs(UnmanagedType.LPTStr)] public String f1;
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst=256)] public String f2;
}

Если нужно передавать строки фиксированной длины в ANSI или Unicode-кодировке, для этого можно воспользоваться массивом с подходящим типом элемента.

Строки фиксированной длины

Как уже говорилось, при некоторых обстоятельствах может потребоваться передать неуправляемому коду буфер строк фиксированной длины для дальнейшей обработки. Остановимся на этом подробнее. В качестве примера возьмем функцию GetWindowText, определенную в Windows.h. lpString указывает на буфер размером nMaxCount, расположенный на вызывающей стороне. От вызывающей стороны ожидается, что она разместит буфер и установит значение nMaxCount, соответствующее размеру размещенного буфера.

int GetWindowText(
  HWND hWnd,        // handle окна или элемента управления
  LPTSTR lpString,  // текстовый буфер
  int nMaxCount     // максимальное число копируемых символов
);

Использование класса String в данном случае невозможно, так как вызываемой стороне не разрешено изменять содержание переданного буфера. Даже передача String по ссылке не позволит инициализировать буфер заданного размера. В данном случае решение состоит в передаче в качестве аргумента вместо класса String класса StringBuilder...

<...>

CharSet

Описания методов и структур могут зависеть от платформы, на которой исполняется приложение (Windows 9х или ОС, основанные на NT). Зависимость эта выражается в формате строк. Платформы, основанные на NT, используют в качестве основного формата Unicode, хотя в них и реализованы ANSI-версии всех строковых API-функций. Но все эти функции преобразуют строки в Unicode перед обработкой, что неэффективно. Windows 9х не поддерживает Unicode и вместо Unicode-функций содержит заглушки. Современные программы, написанные на языках 3GL, обычно вынуждены для совместимости работать в ANSI. Это позволяет запускать один и тот же исполняемый файл на любой платформе без перекомпиляции. Однако под NT это не только снижает производительность, но и не позволяет программе воспользоваться преимуществами NT в области поддержки Unicode. C++-программы позволяют использовать тип TCHAR, который автоматически преобразуется в CHAR или WCHAR в зависимости от значения макроса _UNICODE. Но такое преобразование производится в момент компиляции. Поэтому программисты, желающие выпустить Unicode-версию, вынуждены поддерживать две версии программы, что довольно сложно.

Как было сказано выше, .Net позволяет указать, что параметры метода или поле структуры описаны как платформно-зависимые. При этом соответствующий тип строки будет выбираться автоматически.

Чтобы указать, какой тип строки использовать, нужно...

<...>

На платформе Windows существует много схем разыменования (name mangling) для указания соглашений о вызовах и наборов символов. Когда CharSet установлен в CharSet.Auto, символическому имени автоматически присваивается суффикс W или A, в зависимости от использования Unicode или ANSI. Вдобавок, если необходимые символы не найдены, CLR использует соглашения stdcall (например, Sleep может быть _Sleep@4). Коррекцию имен можно подавить, используя параметр ExactSpelling атрибута DllImport.

Передача структур и классов

Ещё одной задачей, требующей отдельного рассмотрения, является передача структур и классов в качестве параметров неуправляемых методов.

Структуры и классы, информация о расположении полей которых явно задана, называются форматированными типами. Информация о раскладке полей содержится в атрибуте StructLayout, имеющем возможные значения LayoutKind.Sequential или LayoutKind.Explicit. LayoutKind.Sequential означает, что члены типа должны располагаться в неуправляемой памяти в том порядке, в каком они появляются в определении управляемого типа. LayoutKind.Explicit используется для указания, что поля располагаются в соответствии с атрибутом FieldOffset, присвоенным каждому полю. Так, типы Point и Rect, определенные в следующем примере, предоставляют информацию о раскладке полей, используя атрибут StructLayout.

using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public struct Point 
{
  public int x;
  public int y;
}   

[StructLayout(LayoutKind.Explicit]
public struct Rect 
{
  [FieldOffset(0)] public int left;
  [FieldOffset(4)] public int top;
  [FieldOffset(8)] public int right;
  [FieldOffset(8)] public int bottom;
}

При маршалинге в неуправляемый код эти форматированные типы передаются как структуры C. Это дает удобный способ вызова неуправляемых API, содержащих структуры в качестве параметров. Например...

<...>

Маршалинг классов в неуправляемый код как С-структур также возможен, при условии фиксированной раскладки полей. Информация о раскладке полей класса также содержится в атрибуте StructLayout. Главная разница между простыми типами и классами с фиксированной раскладкой в том, как производится их маршалинг в неуправляемый код. Структуры передаются по значению (через стек) и, следовательно, любые изменения их содержимого вызываемой стороной не видны вызывающей стороне. Ссылочные типы передаются по ссылке (ссылка на тип передается в стеке) и, следовательно, любые изменения их полей вызываемой стороной видны вызывающей стороне...

<...>

Как вы понимаете, неуправляемый код понятия не имеет, что вместо ожидаемой структуры ему передан указатель на класс. Поэтому даже если все типы, содержащиеся в классе, изоморфны, и передается указатель на данные управляемого класса, неуправляемый код воспринимает этот класс как простую неуправляемую С-структуру (заметьте, именно С, а не С++, так как С-структуры не могут содержать методы). Если у типа есть методы, свойства или события, они недоступны из неуправляемого кода.

<...>

Форматированные типы могут быть переданы вызовам методов COM Interop. На самом деле при экспорте в библиотеку типов управляемые структуры автоматически конвертируются в их IDL-аналоги. Например, простой тип Point станет typedef'ом по имени Point. Все ссылки на простой тип Point из других мест библиотеки типов заменяются на Point Typedef.

typedef struct tagPoint 
{
   int x;
   int y;
} Point;

interface _Graphics 
{
   …
   HRESULT SetPoint ([in] Point p)
   HRESULT SetPointRef ([in,out] Point *p)
   HRESULT GetPoint ([out,retval] Point *p)
}

<...>

Типы массивов

Управляемые массивы

Есть много типов управляемых массивов.   Класс System.Array – это базовый класс для всех типов массивов. У System.Array есть свойства для определения размерности, длины, нижней и верхней границы массива, а также методы доступа, сортировки, поиска, копирования и создания массивов.

Есть также 2 специализированных типа массивов:

Оба типа массивов – динамические...

<...>

Неуправляемые массивы

Неуправляемые массивы – это либо COM SafeArray или массивы C с фиксированной или переменной длиной. SafeArray – это самоописываемые массивы, тип, размерность и границы которых хранятся в ассоциированной с массивом структуре. Массивы C – одномерные типизированные массивы с фиксированной нижней границей, равной 0.

Сервис маршалинга ограниченно поддерживает оба типа массивов.

Передача массивов в .NET

И C-массивы, и SafeArray можно передать из неуправляемого кода в .NET.

<...>

SafeArray

При импорте SafeArray из библиотеки типов в сборку .NET, массив конвертируется в одномерный массив известного типа (т.е. int[]). К элементам массива применяются те же правила конвертации типов, что и к параметрам. Так, например, SafeArray элементов типа BSTR становится управляемым массивом элементов типа String, а SafeArray из Variant'ов становится управляемым массивом элементов типа Object. Тип элементов SafeArray захватывается в именованный аргумент SafeArraySubType, передаваемый атрибуту MarshalAs, применяемому к параметру.

Поскольку размерность и границы массива нельзя узнать из библиотеки типов, размерность принимается за 1, а нижняя граница – за 0. Размерность и границы должны быть указаны в managed signature, создаваемой TlbImp. Если...

<...>

...Нет никаких физических барьеров для маршалинга многомерных или имеющих ненулевую границу массивов в .Net и обратно. Такие ограничения являются надуманными. Можно предположить, что Microsoft двигало либо желание сделать описание более строгим, или простая человеческая лень. Как бы то ни было, это очень неудобное ограничение, в ряде случаев способное породить несовместимость с уже существующим COM-кодом. Скорее всего, на C# и Мanaged С++ с этим удастся справиться, если вручную импортировать функции для работы с SafeArray-ми и производить все преобразования вручную (описывая параметры методов интерфейсов как примитивные типы). Более полную информацию о ручном маршалинге можно почерпнуть из соответствующего раздела MSDN.

C-массивы

При импорте C-массива в сборку .NET из библиотеки типов, массив конвертируется в ELEMENT_TYPE_SZARRAY.

Тип элементов массива берется из библиотеки типов и сохраняется при импорте. Правила преобразования, применяемые к параметрам, применяются и к элементам массива. Так, например, массив из LPStr станет массивом элементов типа String. Тип элементов массива помещается в именованный аргумент ArraySubType, передаваемый атрибуту MarshalAs, и применяется к параметру...

<...>

Передача массивов в COM

Все типы управляемых массивов могут быть переданы из .NET в неуправляемый код. В зависимости от управляемого типа и применяемых к нему атрибутов, к массиву можно обращаться как к SafeArray или как к массиву C.

<...>

ET_SZARRAY

Если метод, содержащий параметр ET_SZARRAY (одномерный массив) экспортируется из сборки .NET в библиотеку типов, параметр array конвертируется в SafeArray данного типа...

<...>

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

ET_ARRAY

Если метод, содержащий параметр ET_ARRAY, экспортируется из сборки .NET в библиотеку типов, параметр array конвертируется в SafeArray данного типа...

<...>

ET_CLASS <System.Array>

Если метод, содержащий параметр System.Array, экспортируется из сборки .NET в библиотеку типов, параметр array конвертируется в интерфейс _Array.

<...>

Взаимодействие COM и .Net

Использование COM-объектов из .Net

Сначала рассмотрим взаимодействие .NET-кода с COM-кодом. .NET-клиент работает с COM-сервером через RCW (runtime-callable wrapper), как показано на рисунке. RCW оборачивает COM-объект и посредничает между ним и CLR-средой, позволяя COM-объекту выглядеть для .NET-клиентов родным .NET-объектом, a .NET-клиенту выглядеть для COM-объекта стандартным COM-клиентом.

<...>

Каждый экземпляр COM-объекта имеет одну уникальную обертку, независимо от количества реализуемых им интерфейсов. Другими словами, один COM-объект, реализующий 5 интерфейсов, имеет только одну обертку. Эта обертка выдает все 5 интерфейсов. Если создано 2 экземпляра COM-объекта, будут созданы 2 экземпляра обертки.

<...>

Разработчик .NET-клиента может создать сборку, содержащую RCW, одним из двух способов. При наличии Visual Studio.NET нужно просто щелкнуть правой кнопкой мыши по секции References проекта и выбрать Add Reference из контекстного меню. При этом появится диалоговое окно, предлагающее выбор из всех зарегистрированных в системе библиотек типов, а заодно и нерегистрированных, путь к которым нужно указать вручную (см. рисунок 4). Если выбрать COM-объект, Visual Studio .NET создаст RCW и добавит его к проекту.

Рисунок 4. Создание RCW.

То же самое можно сделать с помощью утилиты командной строки по имени TlbImp.exe из NET SDK, доступного бесплатно на сайте Microsoft. Код, читающий библиотеку типов и генерирующий RCW, на самом деле живет в классе .NET-runtime с названием System.Runtime.InteropServices.TypeLibConverter. И Visual Studio .NET, и TlbImp.exe внутри себя используют этот класс. Вам тоже можно, если вы, например, пишете средство разработки, или из мазохистских соображений.

Рисунок 5. .NET-клиент, использующий COM

Рисунок 5 показывает пример клиентской .NET-программы, использующей COM-сервер. Скачать код этого примера можно с нашего сайта.

<...>

...Описанный в предыдущих абзацах механизм RCW требует раннего связывания объектов, а это значит, что для создания классов-оберток разработчик должен быть довольно интимно знаком с объектом (предоставляемым библиотекой типов) во время разработки. Это далеко не всегда реально. Например, скриптам нужно позднее связывание, при котором клиент считывает ProgID объекта и вызываемый метод из кода скрипта во время исполнения. Чтобы обеспечить возможность такого связывания, большинство COM-объектов поддерживает интерфейс IDispatch. В подобных ситуациях невозможно создать RCW заранее. Как же .NET справляется с такими ситуациями?

Рисунок 6. Позднее связывание.

.NET Framework позволяет вызывать COM-объект через интерфейс IDispatch, если, конечно, COM-объект реализует этот интерфейс...

<...>

Использование .NET-объектов из COM

.Net Framework является замечательным средством создания COM-объектов. Читатель может удивиться - зачем возвращаться к старому, менее удобному и функциональному? На это может быть несколько причин. Первая причина, по иронии судьбы, это COM+, из которого вырос .Net, но частью которого не стал. В .Net отсутствует (по крайней мере, пока) полнофункциональный сервер приложений, а COM+ как раз является таковым. Было бы просто глупо игнорировать имеющуюся функциональность в ожидании грядущих технологий. Так можно и всю жизнь прождать. Во-вторых, совместимость с COM нужна для интеграции с имеющимися COM-приложениями. Скажем, имеется COM-клиент, использующий 10 COM-объектов, и его функциональность нужно расширить с помощью свежесозданного .NET-объекта. Это тем более резонно, учитывая, что .Net прекрасно справляется с этой задачей. Парадокс заключается в том, что возможности создания COM-объектов в .Net близки к аналогичным возможностям С++, а простота сравнима с VB 6.

При этом .NET Framework прибегает к тактике, похожей на используемую при импорте COM-объектов. Однако... при этом оборачивается не COM-, а .Net-объект, а обертка является не .Net-объектом, а неуправляемым кодом, эмулирующим COM-объект. Такая обертка носит название CCW (COM-callable wrapper) (см. рисунок 7).

Рисунок 7. COM-callable Wrapper

<...>

Рисунок 8.

Функции создания COM-объектов не знают, как передавать параметры создаваемым объектам, поэтому создаваемый класс заведомо не должен нуждаться в таких параметрах. .NET-класс может иметь любое количество параметризованных конструкторов для использования .NET-клиентами, при условии, что есть еще один, без параметров, для использования COM-клиентами.

Чтобы COM-клиент смог найти .NET-объект, требуется создать вхождения реестра, нужные COM-клиенту для обнаружения сервера при создании объекта. При наличии VS.Net можно просто установить автоматическую регистрацию .NET-объекта для COM Interop (см. рисунок 8).

При этом, если заранее не дать сборке строгого имени, будет выдан диалог, показанный на рисунке 9.

Здесь нужно нажать ОК, и VS.Net сформирует строгое имя сборки.

Рисунок 9.

Остается написать код .Net-компонента...

<...>

...Вся эта простота обеспечивается VS.Net. Ручная регистрация несколько сложнее. Придется пойти другим путем и использовать утилиту с названием RegAsm.exe, входящую в состав .NET SDK. Эта программа читает метаданные в .NET-классе и создает вхождения реестра, нужные COM-клиентам. Пример содержит batch-файл, выполняющий за пользователя регистрацию с помощью RegAsm.exe, и содержащий единственную строку:

regasm dotnetserverforcomclient.dll /tlb

Рисунок 10. Вхождения реестра, создаваемые RegAsm.exe.

Создаваемые им вхождения реестра приведены на рисунке 10. Заметьте, что значение ключа InProcServer32 COM-сервера содержит Mscoree.dll, то есть, при попытке создания COM-объекта через CoCreateInstance будет загружена именно эта DLL...

<...>

Зарегистрировать .NET-компонент, живущий в GAC, сложно из-за сложной структуры каталогов GAC. Explorer содержит дополнительный модуль, упрощающий ее представление. Но если посмотреть на эту структуру из окна командной строки, можно увидеть множество уровней вложенности, разобраться с которыми не так просто.

Рисунок 11. Вид одного элемента GAC из WinCmd (обратите внимание на путь в заголовке окна).

Рисунок 12. Вид GAC из Explorer.

При ручной регистрации наиболее удобным может оказаться размещение .NET-компонентов в стандартном каталоге, запуск regasm.exe из этого каталога, и использование gacutil.exe для перемещения компонентов в GAC. Если перемещение COM-компонента после регистрации делает его бесполезным, то для .NET это не так. На самом деле, DLL Hell – это одна из проблем, которую должна устранить архитектура .NET, и она это делает. (как и все остальные архитектуры и технологии Микрософт – прим.ред.) Все разделяемые компоненты живут в GAC, используя строгие имена для исключения конфликтов имен. Загрузчик .NET проверяет наличие запрашиваемого компонента в дереве каталогов приложения, а затем проверяет GAC, если не смог найти нужной сборки.

COM-клиент работает с .NET-объектом как с обычным COM-объектом...

<...>

В общем, если стоит задача создания COM-объектов, проще сперва определить типы в CLR-языке, а затем создать TLB. Если отнестись к листингу 6 как к псевдо-IDL файлу, его можно пропустить через csc.exe и tlbexp.exe, и получить TLB, приведенную в листинге 7 и функционально идентичную получаемой при компиляции настоящего IDL-файла. Преимущества использования такого подхода в том, что определения типов расширяемы и легко читаются, чего нельзя сказать ни о TLB, ни об IDL. Единственным средством со столь же легко читаемым синтаксисом является VB 6, но возможности его куда скромнее.

Листинг 6. C# как лучший IDL

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

[assembly: Guid("4c5025ef-3ae4-4128-ba7b-db4fb6e0c532") ]
[assembly: AssemblyVersion("2.1") ]

namespace AcmeCorp.MathTypes 
{
  [ 
    Guid("ddc244a4-c8b3-4c20-8416-1e7d0398462a"),
    InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
  ]
  public interface ICalculator
  {
    double CurrentValue { get; }
    void   Clear();

    void Add(double x);
    void Subtract(double x);
    void Multiply(double x);
    void Divide(double x);
  }
}

Листинг 7. Созданная TLB

[
  uuid(4C5025EF-3AE4-4128-BA7B-DB4FB6E0C532),
  version(2.1)
]
library AcmeCorp_MathTypes
{
  importlib("stdole2.tlb");
  [
    object,
    uuid(DDC244A4-C8B3-4C20-8416-1E7D0398462A),
    oleautomation,
    custom({0F21F359-AB84-41E8-9A78-36D110E6D2F9}, 
           "AcmeCorp.MathTypes.ICalculator")    
  ]
  interface ICalculator : IUnknown 
  {
    [propget] HRESULT CurrentValue([out, retval] double* pRetVal);
    HRESULT Clear();
    HRESULT Add([in] double x);
    HRESULT Subtract([in] double x);
    HRESULT Multiply([in] double x);
    HRESULT Divide([in] double x);
  }
}

<...>

Следует знать, что REGASM.EXE использовать не обязательно, запросто можно работать с CLR из обычного кода и использовать AppDomain по умолчанию для загрузки типов и создания экземпляров любого класса. В листинге 8 приведен пример этого на VB 6, использующий CLR-класс System.Collections.Stack. Это можно автоматизировать, используя моникер.

<...>

Листинг 8. Работа с CLR из VB 6.

' требуется ссылка на mscorlib.tlb и mscoree.tlb
Dim rt As mscoree.CorRuntimeHost
Dim unk As IUnknown
Dim ad As ComRuntimeLibrary.AppDomain
Dim s As ComRuntimeLibrary.Stack

Private Sub Form_Load()
  Set rt = New mscoree.CorRuntimeHost
  rt.Start
  rt.GetDefaultDomain unk
  Set ad = unk
  Set s = ad.CreateInstance("mscorlib", _
                            "System.Collections.Stack").Unwrap
  s.Push "Hello"
  s.Push "Goodbye"
  s.Push 42
  MsgBox s.Pop()
  MsgBox s.Pop()
  MsgBox s.Pop()
End Sub

<...>

Тип Object

Параметры и поля, явно объявленные как System.Object, могут быть переданы неуправляемому миру либо как Variant, либо как интерфейс. По умолчанию Object преобразуется в COM-Variant. Эти правила применимы только к System.Object, но не к строго типизированным объектам (классам, определенным пользователем), унаследованным от System.Object.

<...>

Импорт и экспорт

Рассмотрим следующее определение управляемого интерфейса:

interface MarshalObject 
{
   void SetVariant(Object o);
   void SetVariantRef(ref Object o);
   Object GetVariant();

   void SetIDispatch ([MarshalAs(UnmanagedType.IDispatch)]Object o);
   void SetIDispatchRef([MarshalAs(UnmanagedType.IDispatch)]ref Object o);
   [MarshalAs(UnmanagedType.IDispatch))] Object GetIDispatch();

   void SetIUnknown ([MarshalAs(UnmanagedType.IUnknown)]Object o);
   void SetIUnknownRef([MarshalAs(UnmanagedType.IUnknown)]ref Object o);
   [MarshalAs(UnmanagedType.IUnknown))] Object GetIUnknown();
}

Определенный выше интерфейс MarshalObject может быть экспортирован в библиотеку типов...

<...>

Маршалинг типа System.Object в интерфейс

Когда ссылка на объект передается (в виде интерфейса) в COM-объект, передается указатель на интерфейс _Object. COM-клиенты могут динамически вызывать члены управляемого класса или любые члены, реализованные его подклассами, через интерфейс _Object. Клиент может также вызвать QueryInterface для получения любых других интерфейсов, явно реализованных управляемым типом. Если для .Net-класса определен атрибут ClassInterface(...), то, как уже говорилось ранее, для класса создается интерфейс класса, который, в зависимости от параметров атрибута, может быть disp- или dual-интерфейсом. При этом именно он становится default-интерфейсом, а _Object описывается как дополнительный интерфейс, реализуемый CoClass'ом.

<...>

Маршалинг объекта в Variant

При маршалинге объекта в Variant, внутренний тип Variant-а (значение поля vt) определяется во время исполнения по следующим правилам:

Если объектная ссылка равна null (null используется в C# и С++, в VB для этого используется Nothing), производится маршалинг объекта в Variant с полем vt, установленным в VT_EMPTY.

Если объект – это экземпляр любого из перечисленных в нижеприведенной таблице типов, результирующий тип Variant'а определяется встроенными в маршалер правилами и приведен в той же таблице.

Другие объекты, желающие явно контролировать поведение маршалера, могут реализовать интерфейс IConvertible. В этом случае тип Variant определяется по коду типа, возвращаемому IConvertible.GetTypeCode.

В остальных случаях производится маршалинг объекта в Variant типа VT_UNKNOWN.

Маршалинг системных типов в Variant

Простые системные типы, приведенные в таблице ниже, автоматически конвертируются в типы variant при маршалинге в качестве объектов. Эти преобразования производятся только если сигнатура вызываемого метода имеет тип System.Object.

<...>

Маршалинг COM-типов, не имеющих соответствующих управляемых типов, может производиться с помощью классов-оберток, например, ErrorWrapper, IDispatchWrapper, IUnknownWrapper или CurrencyWrapper.

<...>

Маршалинг объектов, реализующих интерфейс IConvertible, в Variant

Типы, не перечисленные выше, могут управлять маршалингом, реализуя интерфейс IConvertible.

<...>

Маршалинг Variant в Object

При маршалинге Variant в Object, тип, а иногда и значение передаваемого Variant определяет тип создаваемого объекта.

<...>

In-Out маршалинг

Стоит заметить, что Variant при передаче из COM в .NET, а затем обратно в COM может не сохранить тип. Рассмотрим, что происходит, когда Variant типа VT_DISPATCH передается из COM в .NET. В процессе маршалинга Variant конвертируется в System.Object. Если объект впоследствии передается обратно в COM, Object превратится в Variant типа VT_UNKNOWN. Нет никакой гарантии, что Variant, получающийся при маршалинге объекта из .NET в COM, будет иметь тот же тип, что и Variant, использовавшийся при создании объекта.

Маршалинг Variant при передаче по ссылке

В то время, как сами Variant'ы могут передаваться по значению или по ссылке, флаг VT_BYREF можно также использовать с Variant'ом любого типа, чтобы указать, что содержимое Variant передается по ссылке, а не по значению.

Разница между маршалингом Variant по ссылке (по указателю, в терминах С/С++) и маршалингом с установленным флагом VT_BYREF может обескуражить.

<...>

При передаче объектов из .NET в COM, содержание объектов копируется в новый Variant, создаваемый маршалером согласно правилам, определенным в разделе "Маршалинг объекта в Variant". Изменения Variant на неуправляемой стороне не распространяются на исходный объект при завершении вызова. Аналогично, при передаче Variant из COM в .NET, содержание Variant копируется в свежесозданный объект по правилам, определенным в разделе "Маршалинг объекта в Variant". Изменения объекта на управляемой стороне не отражаются на исходном Variant при завершении вызова.

Чтобы добиться обратной связи, нужно передавать параметры по ссылке. Например, в C# для передачи параметров по ссылке используется ключевое слово ref, а в VB.Net ByRef. Как уже говорилось выше, ref-параметры передаются в COM по указателю, то есть как VARIANT *.

При передаче объекта в COM по ссылке маршалер создает новый Variant. Содержание объектной ссылки копируется в Variant перед вызовом. Variant передается неуправляемой функции, где пользователь может изменить его содержание. После завершения вызова любые изменения Variant на неуправляемой стороне отражаются на исходном объекте. Если тип Variant отличается от переданного в вызове типа, изменения приводят к появлению объекта другого типа. Другими словами, тип переданного в процессе вызова может отличаться от типа возвращаемого объекта.

При передаче Variant в .NET по ссылке маршалер создает новый объект и копирует в него содержание variant до совершения вызова. Ссылка на объект передается управляемой функции, где пользователь может изменить его содержимое. После завершения вызова любые изменения объекта отражаются на исходном Variant. Если тип объекта отличается от типа переданного (в процессе вызова) объекта, тип исходного Variant изменяется. Другими словами, тип Variant до и после вызова может быть разным.

<...>

Передача структур и классов

Передача структур в COM-объекты ничем не отличается от передачи структур в P/Invoke. Передача структур внутри Variant'а не поддерживается (по крайней мере, в beta 2).

Обработка ошибок

У COM и .NET свои подходы к обработке ошибок. Разработчики COM-серверов оповещают вызывающую сторону о возникновении ошибочной ситуации, возвращая значения, отличные от нуля. Положительные значения означают предупреждение, отрицательные – ошибку. Есть целая идеология формирования этих значений, и ее подробное описание выходит за рамки статьи. Если вы хотите подробнее разобраться в этом, обратитесь к MSDN.

<...>

Вызов функции COM API, не входящей в состав интерфейсов

Все методы COM-интерфейсов обязаны возвращать HRESULT, поддерживая обработку COM-ошибок. В disp-интерфейсах для этого используется несколько другая техника, но, как бы то ни было, ошибки обрабатываются. А вот при вызове отдельных функций, возвращающих HRESULT, по умолчанию P/Invoke рассматривает HRESULT просто как 32-битное целое. Это требует от программиста вручную проверять успешность вызова. Более удобный способ вызвать такую функцию – задать в атрибуте DllImport параметр PreserveSig=false. Это заставит слой P/Invoke рассматривать 32-битное целое как COM HRESULT и возбудит COMException в случае неудачного результата (Заметьте, что параметр, называвшийся в Beta 1 TransformSig, в Beta 2 переименован в PreserveSig, а действие его изменено на прямо противоположное). Если рассмотреть два объявления, приведенные в Листинге 11, вызов OLE32Wrapper.CoImpersonateClient1 требует от программиста вручную проверить результат и явно обработать неудачные HRESULT. Благодаря же использованию параметра PreserveSig, вызов OLE32Wrapper.CoImpersonateClient2 говорит CLR отображать отрицательные значения HRESULT (означающие неисправимые ошибки) на COMExceptions (означают неисправимые ошибки), не требуя вмешательства программиста. В случае OLE32Wrapper.CoImpersonateClient2 метод возвращает void, поскольку возвращенного нижележащей функцией значения не существует. Если бы при декларации метод был объявлен как возвращающий типизированное значение, скажем, double, слой P/Invoke предположил бы, что нижележащая функция принимает дополнительный параметр по ссылке (а ля COM-атрибут [retval]). Такое отображение имеет место, только если параметр PreserveSig установлен в False.

Листинг 11. Использование DllImport с PreserveSig

using System.Runtime.InteropServices;

public class OLE32Wrapper 
{
  [DllImport("ole32.dll", EntryPoint="CoImpersonateClient")]
  public extern static int CoImpersonateClient1();

  [DllImport("ole32.dll", EntryPoint="CoImpersonateClient", PreserveSig= False)]
  public extern static void CoImpersonateClient2();
}

.NET и COM+

На сегодняшний день .Net Framework не предоставляет в распоряжение разработчика никакого сервера приложений. Но существуют два готовых сервера приложений, IIS и COM+, с которыми .Net прекрасно интегрируется. С IIS .Net интегрируется с помощью ASP.Net и Web-сервисов (об этом можно подробнее прочитать в статье по Web- сервисам в этом номере журнала – прим.ред.), но это отдельная проблема. А вот об интеграции с COM+ мы сейчас поговорим подробнее...

<...>

Транзакции

COM+ и его предшественник, Microsoft Transaction Services (MTS), предоставляют автоматическую поддержку, упрощающую программистам создание объектов, участвующих в транзакциях. Программист административно помечает свои объекты как требующие транзакции. Вслед за этим COM+ автоматически создает транзакцию при активации объекта. Для внесения изменений в БД объект использует COM+ Resource Managers, программы типа SQL Server, поддерживающие транзакции COM+. Затем объект сообщает COM+, доволен ли он результатами. Если все участвующие в транзакции объекты удовлетворены, COM+ фиксирует транзакцию, сохраняя все изменения. Если какой-нибудь объект возражает, COM+ откатывает транзакцию, отменяя результаты всех действий объектов и откатывая состояние системы до исходного.

Родные .NET-объекты также могут участвовать в транзакциях. Поскольку существующая микрософтовская система работы с транзакциями основана на COM, .NET-объекты делают это используя свои возможности взаимодействия с COM, описанные выше. Вы регистрируете .NET-объект как COM-сервер. Затем с помощью COM+ Explorer эти компоненты инсталлируются в COM+ приложение, и их транзакционные настройки задаются так же, как и для COM-компонентов. Для регистрации и настройки COM+ приложений за один шаг можно использовать и средство командной строки Regsvcs.exe из .NET SDK.

В качестве альтернативы, так же, как родные COM-компоненты иногда содержат транзакционные настройки в библиотеке типов, так и транзакционные настройки .NET-объектов можно указать в их метаданных... Разница заключается только в том, что...

<...>

.NET-объект, участвующий в транзакции, должен голосовать по исходу транзакции. Это можно сделать двумя способами. В COM+ и MTS объект выбирал свой контекстный объект, вызывая API-функцию CoGetObjectContext или обертку для нее, GetObjectContext, затем вызывал метод контекстного объекта, чтобы высказать свое мнение по исходу транзакции. .NET-объект находит свой контекст в предоставляемом системой объекте System.EnterpriseServices.ContextUtil. Этот объект предоставляет часто используемые методы SetAbort и SetComplete и их несколько реже используемых собратьев, EnableCommit и DisableCommit. Методы EnableCommit и DisableCommit задают биты успешности (happy bit) и завершенности (done bit) точно так же, как это делалось в COM+. Контекст также содержит свойства DeactivateOnReturn и MyTransactionVote, которые позволяют читать и задавать эти биты по отдельности. Вместо этого можно также задать голосование типа "fire and forget", пометив .NET-объект атрибутом AutoComplete. Если сделать так, нормальный выход из объекта автоматически вызовет SetComplete, а любое исключение автоматически вызовет SetAbort. Пожалуй, это самый простой, но не всегда приемлемый вариант работы.

ActiveX-элементы в .NET-контейнерах

Если бы .NET не мог использовать ActiveX-элементов, многие разработчики не стали бы его использовать для создания клиентских приложений. Поэтому создатели .NET Windows Forms были вынуждены включить поддержку ActiveX-элементов.

Приложение Windows Forms в действительности не знает, как использовать ActiveX-элемент. Оно понимает только элементы управления, написанные в родной ему .NET-архитектуре. Чтобы приложение Windows Forms смогло включать в себя ActiveX-элемент, нужно сгенерировать содержащий ActiveX-элемент класс-обертку, посредничающий между COM-мировоззрением элемента и .NET-убеждениями контейнера, и представляющий его Windows Forms как родной элемент управления. Это должен быть монстроидальный класс-обертка, вызываемый в процессе исполнения, как описано выше в этой статье, принимающий все COM-интерфейсы, выдаваемые ActiveX-элементом, и выдающим COM-интерфейсы, требуемые ActiveX-элементом от контейнера. Такая обертка похожа на CAxWindow в ATL или TOleControl в Delphi. Эта архитектура показана на рисунке 10. Если вы считаете, что это требует прорвы усилий, вы правы, но волноваться не нужно – CLR дает для этого готовый класс System.Windows.Forms.AxHost.

Рисунок 13. Windows Forms

<......>

Стратегия переноса

Для переноса кода на CLR есть три основные стратегии. Рисунок 17 демонстрирует пример частичного переноса, когда исходный код механически переводится со старого (например, Visual Basic 6.0) языка на CLR-язык (Visual Basic .NET или C#). Этот подход базируется на возможности импорта в CLR любых зависимых библиотек с помощью interop-сервисов.

Рисунок 17. Частичный перенос кода

На рисунке 18 приведен пример полного переноса, когда исходный код полностью переписывается на CLR-языке и не связан ни с какими старыми компонентами. Эта стратегия подразумевает наличие управляемых версий всех связанных библиотек. Она также подразумевает вашу готовность изменять исходный код из-за стилистических различий, порождаемых управляемыми библиотеками. Этот подход наиболее трудоемок, но имеет явное преимущество – меньшее количество interop-переходов и (как правило, хотя и не всегда) более богатые и лучше спланированные библиотеки.

Рисунок 18. Полный перенос кода

Рисунок 19 иллюстрирует подход, при котором исходный код оставляют в покое, и импортируют код в CLR с помощью interop-сервисов. В этом случае не нужно ни наличия управляемых версий библиотек, ни существенных изменений кода. Это самый "ленивый" подход, который может, тем не менее, оказаться выгодным из-за уменьшения количества interop-переходов вследствие меньшего количества вызовов связанных библиотек на метод.

Рисунок 19. Импорт через interop-сервисы

Советы по созданию более совместимого неуправляемого кода.

Итак, чтобы ваш код можно было легко импортировать в .Net, лучше всего пользоваться следующими правилами.

Организовывать свой код в виде COM-библиотек, размещаемых в DLL.

Стараться делать все интерфейсы dual-интерфейсами или помечать их атрибутом oleautomation.

Обеспечить совместимость c OLE Automation, так чтобы все типы были совместимы с этой спецификацией. Убедиться, что ваша библиотека совместима с OLE Automation можно, включив в описание всех интерфейсов атрибут oleautomation. При этом MIDL будет выдавать предупреждения на большинство некорректно описанных типов, параметров и других элементов. Единственное, на что можно не обращать внимания – MIDL может выдавать предупреждения на параметры методов, описанные как указатели на интерфейс. Последняя версия OLE Automation прекрасно поддерживает использование типизированных интерфейсов при описании типов параметров.

Не забывать указывать атрибуты in, out для параметров.

Развивая пункт 3, хочется сказать, что для строк лучше использовать только тип BSTR.

Для передачи массивов лучше использовать тип SAFEARRAY и как огня избегать определения массивов с использованием атрибутов size_is, length_is и т.п. Значения этих атрибутов не помещаются в библиотеку типов, и у вас будут большие неприятности при импорте интерфейсов, содержащего такие методы. При определении массива с помощью SAFEARRAY старайтесь делать одномерные массивы с нулевой нижней границей.

Используйте атрибут retval для указания, что метод должен восприниматься как функция.

Не злоупотребляйте возвратом значений HRESULT, отличных от 0. .Net, как и VB 6, игнорирует положительные значения HRESULT, и у вас не будет никакой возможности доступа к этому значению. С++-программисты довольно часто используют такую технику, чтобы передавать дополнительную информацию без создания дополнительных параметров или промежуточных переменных. Вы можете и дальше поступать так, но всегда нужно иметь [out] или [out, retval]-параметр, который будет возвращать это значение в управляемый код или в VB 6. Например, для облегчения жизни вы можете воспользоваться следующей тактикой: определять, не равен ли параметр, в который нужно возвратить значение, NULL (ведь все [out]-параметры должны передаваться по указателю), и, если равен, игнорировать его, возвращая значение как положительный HRESULT. В противном случае следует возвращать значение в этом параметре. Ниже приведен соответствующий код:

HRESULTSomeFunc(/*[out, retval]*/ long * pVal)
{
  if(pVal)
    *pVal = 12345;
  else
    return 12345;
  return S_OK;
}

Не следует злоупотреблять возвратом отрицательных значений, так как ошибки (возбуждаемые таким образом в COM) преобразуются .Net в исключения. Модель исключений в .Net рассчитана на успешное выполнение большей части кода. Сама обработка приводит к непроизводительным затратам и неудобству для .Net-программистов.

Не забывайте, что COM-объекты должны реализовывать интерфейс IProvideClassInfo2. Это позволяет .Net очень быстро идентифицировать класс объекта.

Старайтесь называть методы и типы COM-объектов покороче. В языках, совместимых с CLR, очень часто используется "многоточковая" нотация, т.е. очень популярны записи такого вида:

System.CodeDom.CodeArgumentReferenceExpression.

Учитывайте особенности управления памятью, связанные с ленивым освобождением памяти сборщиком мусора. Добавляйте методы, позволяющие явно освобождать ресурсы, занятые COM-объектами.

Каждый COM-объект, импортируемый в .Net, наследуется от класса System.Object. Поэтому не создавайте методов, совпадающих по имени с методами этого класса.

Не включайте в библиотеки типов описаний глобальных функций. Они игнорируются при импорте библиотеки типов. В принципе, это не смертельно, но вам придется описывать их вручную средствами P/Invoke.

Большинство из этих советов могут пригодиться для совместимости не только с .Net, но и с другими языками программирования. Практически любой язык или средство разработки смогут использовать библиотеку, соответствующую этим нехитрым правилам. Исключение составляют скриптовые языки, которым массивы нужно передавать как VARIANT, содержащий внутри себя SAFEARRAY.

Заключение

Возможно, еще по прошествии достаточного времени во многих программах, созданных с помощью .Net, будет оставаться немало неуправляемого кода. В конце концов, появление .Net не отменяет всего, что было создано до него. Многие разработчики, возможно, вообще не захотят переходить на новую платформу. Interop дает возможность использовать библиотеки, совместимые с другими платформами и средствами разработки, что позволяет как сгладить переход на новую платформу, так и обеспечить мирное сосуществование двух миров.

**полностью статью можно прочитать в печатной версии журнала

Copyright © 1994-2016 ООО "К-Пресс"