Взаимодействие .NET с неуправляемым кодомИсточник: rsdn Алифанов Андрей
ВведениеПервоначально цель написания данной статьи заключалась в следующем: показать, как писать обертки для низкоуровневых интерфейсов на языках семейства 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, или берется из описания самой функции. Во втором случае подразумевается, что ее название в программе соответствует ее имени в библиотеке. Пример использования этого атрибута приведен ниже:
Это все, что касается только технологии 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 приведен ниже:
Можно видеть, что в данном случае строка szDescription передается в виде массива фиксированной длины из Unicode-символов. Для маршалинга остальных полей применяются правила по умолчанию.
Обработка ошибок в COM и .NETCOM методы сообщают об ошибках, возвращая соответствующий 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.
Если посмотреть на IDL-описание этого метода, видно, что передается ссылка на CLSID (GUID), по правилам языка C# структуры передаются по значению, а Guid является именно структурой. Поэтому, чтобы правильно передать параметр в COM метод, мало задать атрибут [In], нужно еще указать ключевое слово ref для параметра rcatid. Точно также, для задания выходных параметров нужно не только задавать атрибут [Out], но и ключевое слово out. При несоблюдении этих правил возможны ошибки компиляции или, что хуже, ошибки времени выполнения. Атрибут MarshalAs задает правила передачи параметров, наиболее часто он используется в следующих видах:
Примеры интерфейсовIDL
Описание классовДля описания классов также используются атрибуты ComImport и Guid. Классы с атрибутом ComImport не могут иметь никаких данных и методов. Пример описания класса
Пример использования классаC#
Из этого примера видна еще одна особенность работы с 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. |