Взаимодействие .NET с неуправляемым кодом

Автор: Алифанов Андрей
The RSDN Group
Опубликовано: 15.02.2002
Версия текста: 1.0

Введение
PlatformInvoke
Конвертирование типов
Описание структур - атрибут StructLayout
Обработка ошибок в COM и .NET
Описание интерфейсов – атрибуты ComImport, Guid, InterfaceType
Описания параметров методов – атрибуты In, Out, MarshalAs
Примеры интерфейсов
Описание классов
Пример описания класса
Пример использования класса
Демонстрационное приложение
Заключение

Демонстрационный проект

Введение

Первоначально цель написания данной статьи заключалась в следующем: показать, как писать обертки для низкоуровневых интерфейсов на языках семейства VisualStudio 7.0. Но по мере знакомства с предметом я понял, что тему можно расширить, так как схожие механизмы используются не только для взаимодействия с COM-объектами, но и для взаимодействия с низкоуровневым системным кодом Windows, в частности – с Win32 API. Кроме того, я думаю, что многим будет интересно узнать, как же в действительности выглядит код, который создается утилитами типа TlbImp (я здесь имею в виду код на языке C#, а не реально создающийся код на MSIL).

Эта тема достаточно актуальна для переходного периода, когда существует огромное количество кода, написанного с использованием Win32 API и COM-объектов, с которым нужно взаимодействовать. Проблема несколько смягчается, если используются объекты, описанные в библиотеках типов, за счет использования утилит, автоматически генерирующих сборки. Но что делать, если библиотеки типов нет или код находится в экспортируемой функции некоторой динамической библиотеки? В этом случае выход только один – вручную написать необходимые обертки.

Механизм, используемый для взаимодействия .NET с неуправляемым кодом, достаточно хорошо должен быть знаком тем, кто описывал интерфейсы на языке IDL. В обоих случаях используются атрибуты.

Атрибуты главным образом используются для правильного обмена данными между управляемым (managed) и неуправляемым (unmanaged) кодом, но не только.

PlatformInvoke

Рассмотрение интеграции управляемого и неуправляемого кода начнем с PlatformInvoke. Эта технология позволяет достаточно просто вызывать функции динамических библиотек путем отображения объявления статического метода на точку входа PE/COFF.

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

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

[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW",  SetLastError=true,
           CharSet=CharSet.Unicode, ExactSpelling=true,
           CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);

Это все, что касается только технологии PlatformInvoke. Темы, рассматриваемые дальше, имеют отношение как к PlatformInvoke, так и к общению с COM-объектами из .NET. За исключением, естественно, описаний интерфейсов и классов.

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

Важный вопрос, встающий при взаимодействии управляемого и неуправляемого кода: конвертирование типов. При осуществлении вызова функции ее параметры одновременно являются экземплярами и CLR, и внешнего мира. Здесь важно понимать, что каждый параметр имеет два типа – управляемый и неуправляемый. Кроме того, некоторые типы имеют одинаковый вид и в управляемом, и в неуправляемом коде, а это значит, что при их передаче никакого преобразования не требуется. К таким типам относятся следующие: Single, Double, SByte, Byte, Int16, UInt16, Int32, UInt32, Int64, UInt64 и одномерные массивы этих типов. Все остальные типы должны преобразовываться.

Для задания правил конвертирования используется атрибут MarshalAs. Он может применяться к параметрам и результатам методов, полям структур и классов. Этот атрибут не является обязательным, так как каждый тип данных имеет встроенные правила маршалинга. Но если данный тип может быть сконвертирован во множество других типов, необходимо применение этого атрибута.

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

Для примеров я взял реализацию COM-объекта «Менеджера компонентных категорий», а именно структуры CATEGORYINFO и интерфейсов IEnumGUID, IEnumCATEGORYINFO и ICatInformation.

Описание структур - атрибут StructLayout

Применяется ко всей структуре и позволяет управлять физическим расположением членов структуры в памяти. В общем случае CLR управляет расположением данных структур и классов самостоятельно, если же нужно передавать класс или структуру в неуправляемый код, используется атрибут StructLayout.

Поле Pack этого атрибута может иметь следующие значения:

Пример использования атрибутов StructLayout и MarshalAs приведен ниже:

IDL
#define CATDESC_MAX 128
typedef struct tagCATEGORYINFO {
  CATID   catid;
  LCID    lcid;
  OLECHAR szDescription[CATDESC_MAX];
} CATEGORYINFO, *LPCATEGORYINFO;

C#
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Unicode)]
public struct CATEGORYINFO 
{
  public Guid catid;
  public uint lcid;
  [MarshalAs(UnmanagedType.ByValTStr, SizeConst=128)]
  public String szDescription;
};

Можно видеть, что в данном случае строка szDescription передается в виде массива фиксированной длины из Unicode-символов. Для маршалинга остальных полей применяются правила по умолчанию.

ПРИМЕЧАНИЕ

В документации Miscrosoft утверждается, что поле SizeConst атрибута MarshalAs задает размер массива в байтах, на самом деле это поле задает количество элементов в массиве.

Обработка ошибок в COM и .NET

COM методы сообщают об ошибках, возвращая соответствующий HRESULT, .NET методы – генерируя исключения. Здесь возникает одна проблема - .NET игнорирует любые положительные значения HRESULT, что приводит к неправильной работе перечислителей типа IEnumXXX, так как последние сигнализируют о достижении конца последовательности возвратом значения S_FALSE = 1. Чтобы решить эту проблему - для методов введен атрибут PreserveSig. Задание этого атрибута позволяет подавить генерацию исключений .NET, и гарантирует возврат точного значения HRESULT из COM метода, в противном случае результатом метода всегда будет S_OK = 0. Пример использования этого атрибута приведен ниже.

Описание интерфейсов – атрибуты ComImport, Guid, InterfaceType

Для описания интерфейсов и классов применяются атрибуты ComImport и Guid. Атрибут ComImport – показывает, что тип был ранее определен в COM. CLR обращается с такими типами не так, как с «родными», в частности – по другому создает объекты таких типов, выполняет приведение типов, удержание объектов в памяти и т.д. Этот атрибут обязательно сопровождается атрибутом Guid, название которого говорит само за себя.

Атрибут InterfaceType применяется для описания базового COM интерфейса и может принимать следующие значения: дуальный, IDispatch или IUnknown. Если этот атрибут опущен, то считается, что интерфейс дуальный. В нашем случае все интерфейсы наследуют от IUnknown.

Описания параметров методов – атрибуты In, Out, MarshalAs

Параметры могут передаваться разными способами. Правильное описание параметров определяется не только атрибутами, но и модификаторами языка C#.

Для примера рассмотрим метод ICatInformation.GetCategoryDesc.

void ICatInformation.GetCategoryDesc([In] ref Guid rcatid,
                                     [In] uint lcid,
                                     [Out, MarshalAs(UnmanagedType.LPWStr)] out String pszDesc);

Данный метод можно описать в виде функции:

[return : MarshalAs(UnmanagedType.LPWStr)] String ICatInformation.GetCategoryDesc([In] ref Guid rcatid, [In] uint lcid);

Такой синтаксис можно использовать для функций Win32API и методов COM-интерфейсов, имеющих последний параметр типа out и возвращающих HRESULT. Далее, в примерах интерфейсов и в демонстрационном приложении методы будут записываться подобным образом. Модификатор return нужен только при задании атрибута MarshalAs для методов COM-интерфейсов.

Если посмотреть на IDL-описание этого метода, видно, что передается ссылка на CLSID (GUID), по правилам языка C# структуры передаются по значению, а Guid является именно структурой. Поэтому, чтобы правильно передать параметр в COM метод, мало задать атрибут [In], нужно еще указать ключевое слово ref для параметра rcatid. Точно также, для задания выходных параметров нужно не только задавать атрибут [Out], но и ключевое слово out. При несоблюдении этих правил возможны ошибки компиляции или, что хуже, ошибки времени выполнения.

Атрибут MarshalAs задает правила передачи параметров, наиболее часто он используется в следующих видах:

Примеры интерфейсов

IDL
[
  object,
  uuid(0002E000-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface IEnumGUID : IUnknown
{
  HRESULT Next([in] ULONG celt,
               [out, size_is(celt), length_is(*pceltFetched)] GUID *rgelt,
               [out] ULONG *pceltFetched);

  HRESULT Skip([in] ULONG celt);

  HRESULT Reset();

  HRESULT Clone([out] IEnumGUID **ppenum);
}

[
  object,
  uuid(0002E011-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface IEnumCATEGORYINFO : IUnknown
{
  HRESULT Next([in] ULONG celt,
               [out, size_is(celt), length_is(*pceltFetched)] CATEGORYINFO *rgelt,
               [out] ULONG *pceltFetched);

  HRESULT Skip([in] ULONG celt);

  HRESULT Reset();

  HRESULT Clone([out] IEnumCATEGORYINFO **ppenum);
}

[
  object,
  uuid(0002E013-0000-0000-C000-000000000046),
  pointer_default(unique)
]
interface ICatInformation : IUnknown
{
  HRESULT EnumCategories(
	[in] LCID lcid,
	[out] IEnumCATEGORYINFO** ppenumCategoryInfo);

  HRESULT GetCategoryDesc(
	[in] REFCATID rcatid,
	[in] LCID lcid,
	[out] LPWSTR* pszDesc);

  [local]
  HRESULT EnumClassesOfCategories(
	[in] ULONG cImplemented,
	[in,size_is(cImplemented)] CATID rgcatidImpl[],
	[in] ULONG cRequired,
	[in,size_is(cRequired)] CATID rgcatidReq[],
	[out] IEnumCLSID** ppenumClsid);

  [call_as(EnumClassesOfCategories)]
  HRESULT RemoteEnumClassesOfCategories(
	[in] ULONG cImplemented,
	[in,unique,size_is(cImplemented)] CATID rgcatidImpl[],
	[in] ULONG cRequired,
	[in,unique,size_is(cRequired)] CATID rgcatidReq[],
	[out] IEnumCLSID** ppenumClsid);

  [local]
  HRESULT IsClassOfCategories(
	[in] REFCLSID rclsid,
	[in] ULONG cImplemented,
	[in,size_is(cImplemented)] CATID rgcatidImpl[],
	[in] ULONG cRequired,
	[in,size_is(cRequired)] CATID rgcatidReq[]);

  [call_as(IsClassOfCategories)]
  HRESULT RemoteIsClassOfCategories(
	[in] REFCLSID rclsid,
	[in] ULONG cImplemented,
	[in,unique,size_is(cImplemented)] CATID rgcatidImpl[],
	[in] ULONG cRequired,
	[in,unique,size_is(cRequired)] CATID rgcatidReq[]);

  HRESULT EnumImplCategoriesOfClass(
	[in] REFCLSID rclsid,
	[out] IEnumCATID** ppenumCatid);

  HRESULT EnumReqCategoriesOfClass(
	[in] REFCLSID rclsid,
	[out] IEnumCATID** ppenumCatid);
}

C#
[
  ComImport,
  Guid("0002E000-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface IEnumGUID 
{
  [PreserveSig()]
  int Next([In] uint celt,
           [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Guid[] rgelt,
           [Out] out uint pceltFetched);

  [PreserveSig()]
  int Skip([In] uint celt);

  void Reset();

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID Clone();
};

[
  ComImport,
  Guid("0002E011-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface IEnumCATEGORYINFO 
{
  [PreserveSig()]
  int Next([In] uint celt,
           [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] CATEGORYINFO[] rgelt,
           [Out] out uint pceltFetched);

  [PreserveSig()]
  int Skip([In] uint celt);

  void Reset();

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumCATEGORYINFO Clone();
};

[
  ComImport,
  Guid("0002E013-0000-0000-C000-000000000046"),
  InterfaceType(ComInterfaceType.InterfaceIsIUnknown)
]
public interface ICatInformation 
{
  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumCATEGORYINFO EnumCategories([In] uint lcid);

  [return : MarshalAs(UnmanagedType.LPWStr)]
  String GetCategoryDesc([In] ref Guid rcatid, [In] uint lcid);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID EnumClassesOfCategories([In] uint cImplemented,
                                    [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Guid[] rgcatidImpl,
                                    [In] uint cRequired,
                                    [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=2)] Guid[] rgcatidReq);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID RemoteEnumClassesOfCategories([In] uint cImplemented,
                                          [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Guid[] rgcatidImpl,
                                          [In] uint cRequired,
                                          [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=2)] Guid[] rgcatidReq);

  [PreserveSig()]
  int IsClassOfCategories([In] ref Guid rclsid, 
                          [In] uint cImplemented,
                          [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Guid[] rgcatidImpl,
                          [In] uint cRequired,
                          [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=2)] Guid[] rgcatidReq);

  [PreserveSig()]
  int RemoteIsClassOfCategories([In] ref Guid rclsid, 
                                [In] uint cImplemented,
                                [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=0)] Guid[] rgcatidImpl,
                                [In] uint cRequired,
                                [In, MarshalAs(UnmanagedType.LPArray, SizeParamIndex=2)] Guid[] rgcatidReq);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID EnumImplCategoriesOfClass([In] ref Guid rclsid);

  [return : MarshalAs(UnmanagedType.Interface)]
  IEnumGUID EnumReqCategoriesOfClass([In] ref Guid rclsid);
};

Описание классов

Для описания классов также используются атрибуты ComImport и Guid. Классы с атрибутом ComImport не могут иметь никаких данных и методов.

Пример описания класса

IDL
Описание отсутствует

C#
[
  ComImport,
  Guid("0002E005-0000-0000-C000-000000000046")
]
public class StdComponentCategoriesMgr 
{
};

Пример использования класса

C#
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using ComCatWrapper;

public class Test
{
  static void Main()
  {
    StdComponentCategoriesMgr mgr = new StdComponentCategoriesMgr();
    ICatInformation catInfoItf = (ICatInformation)mgr;
    IEnumCATEGORYINFO enumCInfoItf = сatInfoItf.EnumCategories(0);

    // и т.д.

  }
}

Из этого примера видна еще одна особенность работы с COM-объектами в .NET: вместо привычного CoCreateInstance используется оператор new, а вместо QueryInterface используется приведение типов.

Демонстрационное приложение

Демонстрационное приложение, демонстрирующее работу с COM-интерфейсами, написано на C#. Проект состоит из двух модулей: модуля, обеспечивающего интерфейс пользователя (файл MainForm.cs) и модуля, содержащего обертки COM-объекта «Менеджер категорий компонентов» (файл ComCatWrapper.cs).

Как уже упоминалось, в файле ComCatWrapper.cs содержатся описания структуры CATEGORYINFO и интерфейсов IEnumGUID, IEnumCATEGORYINFO и ICatInformation, а также кокласса StdComponentCategoriesMgr.

Файл MainForm.cs содержит код, необходимый для построения простейшего пользовательского интерфейса и использует интерфейсы из ComCatWrapper.cs.

Первоначально список категорий пуст, он заполняется при нажатии кнопки «Заполнить». Так сделано из-за того, что заполнение идет достаточно долго, а использование, например, дополнительных потоков усложнило бы логику программы.

Вся работа с COM-интерфейсами ведется в двух функциях: FillBtn_Click и FillNodes. Эти функции просты и достаточно подробно прокомментированы.

Визуально категории компонентов представляются в виде дерева следующего вида: описание категории, соответствующий ей идентификатор (CATID) и идентификаторы классов (CLSID), реализующих данную категорию.

Ниже приведен пример работы тестового приложения, использующего обертки COM-интерфейсов


Заключение

Как видим, обеспечить взаимодействие COM и .NET довольно просто для программиста на C#. Нужно только знать, какие параметры и как передавать между управляемым и неуправляемым кодом.

К сожалению, во время подготовки статьи выяснилось, что ManagedC++ и VB.NET не позволяют писать обертки для COM-объектов без использования tlb. Задание атрибута ComImport в этих языках приводит к выбрасыванию исключений при попытке создания экземпляров классов во время выполнения программы, хотя компиляция проходит без проблем. Что это – ошибка или так было задумано, я не знаю. В то же время классы-обертки, написанные на C#, можно использовать и из ManagedC++ и VB.NET.


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