Pull to refresh

.NET в unmanaged окружении – использование и родовые проблемы

Reading time 8 min
Views 7.4K
Managed код и .NET Framework – совершенно замечательная вещь с точки зрения программиста, которому надо кровь из носу выдавать максимально стабильно работающие программы. Использование .NET позволяет очень сильно сократить затраты на разработку, тестирование и сопровождение программных продуктов, особенно по сравнению с C++ или Delphi.

Однако, managed код имеет одну очень серьезную родовую травму, которая прямо проистекает из его достоинств – он изначально несовместим с unmanaged средой, в которой вынужден работать. Boxing, поля памяти, отсутствие прямой адресации и прочие ухищрения, призванные облегчить жизнь программисту, приводят к тому, что взаимодействие managed и unmanaged кода становится проблемой.

Однако нет такой проблемы, которую нельзя решить (пусть даже с помощью топора и лома). Сегодня у нас краткий обзор возможностей организации взаимодействия между managed и unmanaged кодом. Многие C# и особенно VB.NET программисты боятся этого, но на самом деле в этом нет ничего страшного. Начнем мы с самых примитивных методов, которые будут интересны разве что новичкам (поэтому матерые волки .NET могут с чистой совестью первую часть статьи пропустить), и закончим описанием того, что делать, если хочется написать программу на .NET, но сделать это невозможно (а такое тоже бывает). Естественно, к каждому случаю будут приведены конкретные примеры, быть может, хабрачеловеки расскажут мне о моей собственной велосипедности. Параллельно я скажу пару слов о подводных камнях при работе с VSTO и Windows Shell.

Работа с unmanaged-кодом из managed.


Стандартными для такой ситуации с точки зрения .NET являются три механизма, предлагаемых Microsoft – это platform invoke (P/Invoke), COM interop и unsafe.

Общие предостережения


У всех методов работы с unmanaged-кодом есть набор общих недостатков, достаточно серьезных, на которые стоит обратить внимание, и с которыми стоит считаться.
  1. Вызов unmanaged-кода – это опасная операция, которая требует определения допустимости данной операции в манифесте приложения. Это иногда создает проблемы при деплоинге, особенно методами ClickOnce.
  2. Unmanaged-код не является защищенным, не проходит проверок CLR и не следует правилам встроенной системе безопасности .NET. Если вы определили в методе правило безопасности «запретить доступ к файлам» и используете unmanaged-функцию, которая с файлами работает – она отработает без проблем.

Держа в уме данную информацию, перейдем к рассмотрению методов работы с ним.

P/Invoke


Механизм procedure invoke предназначен для обращения к функциям, лежащих в DLL-файлах.

Когда это нужно?


Несмотря на обилие функций, лежащих в библиотеках .NET Framework, некоторые вещи сделать при помощи него все равно принципиально невозможно. Лично мне возможность P/Invoke потребовалась, чтобы управлять mandatory level для файлов и named pipes. Проблема состояла в том, что классы контроля доступа .NET не понимает SDDL-строк, которые содержат описатели mandatory level, и пришлось обращаться к этим функциям напрямую.

Как это выглядит?


Достаточно просто. Объявляем статический класс, в нем прописываем статическую функцию с именем той, которую надо экспортировать, составляем соответствующий список параметров, вешаем атрибут DllImport с именем библиотеки DLL и радуемся жизни.

Пример: импортируем функцию PlaySound

public static class Win32
{
[DllImport("winmm")]
public static extern int PlaySound(string szSound, IntPtr hModule, int flags);
}


* This source code was highlighted with Source Code Highlighter.


Основные проблемы


  1. Передача всевозможных описателей (handles). Проблема решается следующим образом. У большинства объектов существует поле Handle, которое можно передавать в функцию. Если его нет – можно попробовать взять SafeHandle объекта и запросить у него IntPtr через Dangerous-операцию.
  2. Передача структур, указателей на функции и прочих нетривиальных вещей. Действительно, для неспециалиста, который не понимает, как маршаллить данные, это очень большая проблема. К счастью, существует сайт pinvoke.net/, который содержит в себе основные определения для часто используемых функций. Сайт построен на вики-движке и если вам понадобилась системная функция – на 99%, что вы там найдете ее правильное определение.


COM Interop


Действительно мощный механизм, который позволяет использовать в managed-коде COM-компоненты.

Когда это нужно?


В серьезных проектах – достаточно часто. Дело в том, что определенные части системы могут писаться (или уже быть написаны) на С++, Delphi или даже (свят-свят) Visual Basic (тот что не .NET). Кроме того, Win32 сама построена на COM, а значит, предоставляет доступ к функционалу в том числе и через этот механизм. Для того, чтобы пользоваться им без проблем – следует воспользоваться компонентной моделью и COM Interop.

Как это выглядит?


Достаточно просто. Если ваш объект поддерживает automation – то процесс интеропа пройдет без проблем, если нет – возможно придется помучиться.

Чтобы добавить COM-объект в сборку, просто подключите его через References проекта. Библиотека, которая маршалит параметры из unmanaged в managed и обратно, создастся автоматически. Естественно, для сложных объектов, например MAPI или AD, такой интероп работать не будет, в таком случае, следует воспользоваться подходом, о котором я напишу далее.

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

IComExampleInterface iInt = new ComExampleInterface();

* This source code was highlighted with Source Code Highlighter.


Нет, мы не создаем объект интерфейса. На самом деле, у этого интерфейса (это видно в метаданных) есть атрибут под названием default coclass, и именно этот объект и создастся.
Как видите, это очень просто и совсем не страшно.

Основные проблемы


  1. Ошибки при интеропе сложных объектов. .NET не всемогущ, и не всегда может правильно определить, как маршаллятся те или иные данные. В таком случае, опять попадаем в ситуацию, когда следует прописывать маршаллинг явно.
  2. Отсутствие в .NET многих определений стандартных интерфейсов. Например, IUnknown, IDispatch, IDictionary или IStream. Эти определения можно написать руками или выдрать с помощью Reflector из недр ядра .NET (они там есть).
  3. Несовместимость одноименных объектов .NET и Win32. Например, Dictionary в .NET это совсем не то же самое, что объект Scripting.Dictionary Win32. Аналогично с IStream и прочими, поэтому для успешной и плодотворной работы придется писать обертки и экспортировать их. Как? Например, создавая обертки и экспортируя их методами ComExport, о котором далее.


Unsafe-контекст.


Лично мне кажется ненужным атавизмом, но это лично мое мнение.

Когда это нужно?


Чаще всего, применение unsafe-методов мотивируют тем, что подобный подход уменьшает время выполнения кода, особенно в случае работы с большими объемами данных простых типов – в этом случае затраты на boxing/unboxing становятся достаточно велики. Однако, как показывает практика, в реальности программисты, работающие с .NET, все больше оперируют сложными объектами вроде DataSet, кроме того, unsafe требует определения в манифесте приложения специальных полномочий на выполнение небезопасного кода. Лично у меня необходимости применять unsafe не было никогда – даже задачи маршаллинга сложных типов данных с легкостью выполняются с помощью статического класса Marshal с использованием IntPtr.

Как это выглядит?


К описанию метода добавляется ключевое слово unsafe.

public static unsafe void UnsafeMethod(char* chararray)

* This source code was highlighted with Source Code Highlighter.


При этом в методе становятся доступны операции взятия адреса и механизм работы со ссылками, так же, как в С++.
Следует помнить, что unsafe-код остается при этом managed, просто снижается число проверок времени выполнения. Таким образом, он не совсем относится к нашей теме, но упомянуть об этом способе работы лишним не будет.

Основные проблемы


  1. Появление многих наследованных проблем unmanaged-кода, включая главную — переполнение буфера.


Работа с managed-кодом из unmanaged.


Если работа с unmanaged-кодом организована более-менее хорошо, то обратный процесс – обращение к .NET сборкам из unmanaged-кода создает огромное количество проблем. Чтобы понять их суть, рассмотрим механизм, который предлагается Microsoft для релизации подобного подхода.

Механизм этот называется ComExport и позволяет экспортировать объекты managed-среды так, как будто это обычные COM-объекты. Для экспорта объекта, следует определить для него атрибут ComVisible(true), задать CLSID, ProgID, DefaultInterface, провести аналогичную операцию для всех экспортируемых интерфейсов. Особенно важным является определение CLSID и IID для всех экспортируемых интерфейсов, а также их версий, жестко в коде – иначе при разработке приложения любое изменение интерфейса будет приводить к определению нового CLSID и IID, а версия будет постоянно скакать.

  [ComVisible(true)]
  [InterfaceType(ComInterfaceType.InterfaceIsDual)]
  [Guid("5095474B-5273-44ae-A220-9E3820D2EEDC")]
  [ProgId("COM.Export.Test.1")]
  [ComDefaultInterface(typeof(IDefaultInterface))]
  public class Coclass {...}


* This source code was highlighted with Source Code Highlighter.


Однако если бы все было так радужно – никаких проблем бы не было. Реальность гораздо более грустная и существует ряд проблем, которые могут затруднить использование такого подхода.
  1. COM, написанные с использованием .NET требуется регистрировать не с помощью regsvr32, а с помощью regasm, что несколько затрудняет разработку инсталляторов.
  2. Созданные компоненты требуется подписывать (хотя бы временным ключом), либо регистрировать в GAC с помощью gacutil. Регистрация в GAC требует администраторских полномочий в Windows Vista.
  3. С помощью механизма ComExport невозможно создать Out-Process COM.
  4. При использовании таких компонентов, CLR загружается в адресное пространство процесса, который его вызывает.

Остановимся на последних двух пунктах несколько подробнее. Почему же нельзя создать out-process COM?

Если говорить совсем просто, то дело в том, что любое приложение .NET является еще и сборкой, и позволяет подключать себя в качестве простой assembly DLL, без выполнения того когда, который непосредственно прописан в функции main. И если при написании out-process COM, например, на C++, характерным поведением системы является дождаться загрузки приложения, затем подождать, пока она зарегистрирует экспортируемые объекты в ROT, после чего обратиться к нему с запросом фабрики класса, то в случае с .NET приложение не запускается – сборка просто стыкуется к текущему процессу в качестве DLL через посредника. Таким образом, даже если вы в сборке определите синглтон или static-класс – он будет свой для каждого процесса, так как процессы в win32 изолированы друг от друга.

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

А в чем же проблема загрузки CLR в адресное пространство процесса, спросите вы? Ведь .NET поддерживает версионность сборок, и в любом случае будет использована та версия mscorlib, которая соответствует библиотеке.

Не все так просто. Проблемы начнутся тогда, когда вы попытаетесь загрузить COM, написанный с помощью .NET Framework 1.1 в приложении, написанном на .NET Framework 3.5. Две верии одной библиотеки не могут одновременно существовать в одном адресном процессе. Скорее всего, все просто упадет.

Именно этот момент, в частности, прямо запрещает написание таких вещей, как shell extension или namespace extension с помощью .NET. Ведь диалоговое окно сохранения файла или открытия может быть вызвано из любого кода – unmanaged или managed, написанного с помощью любой версии Framework.

Кроме того, положа руку на сердце, ComExport – не самый быстрый механизм. Загрузка CLR – достаточно долгий процесс. Особенно это очевидно в случае разработки add-in к приложениям Microsoft Office с помощью Visual Studio Tools for Office. Средство, планировавшееся как панацея от всех проблем разработки, увязло в одной простой проблеме – с подключенным VSTO add-in время загрузки приложения увеличивается в среднем на величину от 20 до 100 секунд. И если для MS Outlook это не слишком критично, то за увеличение времени загрузки Word или Excel клиенты еще долго будут гоняться за вами со ссаными тряпками :)

Просто запомните – реализовывать shell extensions с помощью .NET нельзя. Не слушайте Microsoft, который в MSDN мягко «не рекомендует» — нельзя. Никогда. Точка.

Для этой проблемы существуют два решения:
  1. Использование proxy-объеков, не выпускающих CLR свои пределы.
  2. Использование custom транспортов для связи между managed и unmanaged кодом.

Когда у меня появилась задача написания namespace extension, которое бы обращалось к серверу, написанному на ASP.NET, который экспортировал API через механизм WCF, проблема для меня встала действительно остро. Налаживать соединения через сокеты, реализовывать механихм аутентификации Windows и парсить SOAP желания не было, а Managed C++ использовать запрещали описанные выше проблемы. Тогда родилась следующая архитектура: namespace extension, написанный на C++, который реализует все интерфейсы Windows Shell, а за данными обращается к приложению, запущенному на локальной (или удаленной) машине, используя канал named pipe. Из этого подхода родилась общая архитектура подобного рода приложений, названная .NET Pipe RPC. Подход действительно достаточно общий, однако у меня в Ворде величина этой статьи составляет уже 5 страниц, поэтому я заканчваю, и если хабрачеловеки заинтересовались этой темой – отпишите в комментариях, и я подробно расскажу об этом подходе с примерами реализации.

Tags:
Hubs:
+29
Comments 32
Comments Comments 32

Articles