|
|
|||||||||||||||||||||||||||||
|
.NET в unmanaged окружении - использование и родовые проблемы (исходники)Источник: habrahabr
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 - это procedure invoke (P/Invoke), COM interop и unsafe.
Общие предостереженияУ всех методов работы с unmanaged-кодом есть набор общих недостатков, достаточно серьезных, на которые стоит обратить внимание, и с которыми стоит считаться.
Держа в уме данную информацию, перейдем к рассмотрению методов работы с ним.
P/InvokeМеханизм procedure invoke предназначен для обращения к функциям, лежащих в DLL-файлах.
Когда это нужно?Несмотря на обилие функций, лежащих в библиотеках .NET Framework, некоторые вещи сделать при помощи него все равно принципиально невозможно. Лично мне возможность P/Invoke потребовалась, чтобы управлять mandatory level для файлов и named pipes. Проблема состояла в том, что классы контроля доступа .NET не понимает SDDL-строк, которые содержат описатели mandatory level, и пришлось обращаться к этим функциям напрямую.
Как это выглядит?Достаточно просто. Объявляем статический класс, в нем прописываем статическую функцию с именем той, которую надо экспортировать, составляем соответствующий список параметров, вешаем атрибут DllImport с именем библиотеки DLL и радуемся жизни. Пример: импортируем функцию PlaySound
Основные проблемы
COM InteropДействительно мощный механизм, который позволяет использовать в managed-коде COM-компоненты.
Когда это нужно?В серьезных проектах - достаточно часто. Дело в том, что определенные части системы могут писаться (или уже быть написаны) на С++, Delphi или даже (свят-свят) Visual Basic (тот что не .NET). Кроме того, Win32 сама построена на COM, а значит, предоставляет доступ к функционалу в том числе и через этот механизм. Для того, чтобы пользоваться им без проблем - следует воспользоваться компонентной моделью и COM Interop.
Как это выглядит?Достаточно просто. Если ваш объект поддерживает automation - то процесс интеропа пройдет без проблем, если нет - возможно придется помучиться. Чтобы добавить COM-объект в сборку, просто подключите его через References проекта. Библиотека, которая маршалит параметры из unmanaged в managed и обратно, создастся автоматически. Естественно, для сложных объектов, например MAPI или AD, такой интероп работать не будет, в таком случае, следует воспользоваться подходом, о котором я напишу далее. После того, как вы подключили компонент, просто создайте новый экземпляр объекта и пользуйтесь им так, как будто это родной объект .NET.
Нет, мы не создаем объект интерфейса. На самом деле, у этого интерфейса (это видно в метаданных) есть атрибут под названием default coclass, и именно этот объект и создастся.
Основные проблемы
Unsafe-контекст.Лично мне кажется ненужным атавизмом, но это лично мое мнение.
Когда это нужно?Чаще всего, применение unsafe-методов мотивируют тем, что подобный подход уменьшает время выполнения кода, особенно в случае работы с большими объемами данных простых типов - в этом случае затраты на boxing/unboxing становятся достаточно велики. Однако, как показывает практика, в реальности программисты, работающие с .NET, все больше оперируют сложными объектами вроде DataSet, кроме того, unsafe требует определения в манифесте приложения специальных полномочий на выполнение небезопасного кода. Лично у меня необходимости применять unsafe не было никогда - даже задачи маршаллинга сложных типов данных с легкостью выполняются с помощью статического класса Marshal с использованием IntPtr.
Как это выглядит?К описанию метода добавляется ключевое слово unsafe.
При этом в методе становятся доступны операции взятия адреса и механизм работы со ссылками, так же, как в С++.
Основные проблемы
Работа с managed-кодом из unmanaged.Если работа с unmanaged-кодом организована более-менее хорошо, то обратный процесс - обращение к .NET сборкам из unmanaged-кода создает огромное количество проблем. Чтобы понять их суть, рассмотрим механизм, который предлагается Microsoft для релизации подобного подхода. Механизм этот называется ComExport и позволяет экспортировать объекты managed-среды так, как будто это обычные COM-объекты. Для экспорта объекта, следует определить для него атрибут ComVisible(true), задать CLSID, ProgID, DefaultInterface, провести аналогичную операцию для всех экспортируемых интерфейсов. Особенно важным является определение CLSID и IID для всех экспортируемых интерфейсов, а также их версий, жестко в коде - иначе при разработке приложения любое изменение интерфейса будет приводить к определению нового CLSID и IID, а версия будет постоянно скакать.
Однако если бы все было так радужно - никаких проблем бы не было. Реальность гораздо более грустная и существует ряд проблем, которые могут затруднить использование такого подхода.
Остановимся на последних двух пунктах несколько подробнее. Почему же нельзя создать 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 мягко "не рекомендует" - нельзя. Никогда. Точка. Для этой проблемы существуют два решения:
Когда у меня появилась задача написания namespace extension, которое бы обращалось к серверу, написанному на ASP.NET, который экспортировал API через механизм WCF, проблема для меня встала действительно остро. Налаживать соединения через сокеты, реализовывать механихм аутентификации Windows и парсить SOAP желания не было, а Managed C++ использовать запрещали описанные выше проблемы. Тогда родилась следующая архитектура: namespace extension, написанный на C++, который реализует все интерфейсы Windows Shell, а за данными обращается к приложению, запущенному на локальной (или удаленной) машине, используя канал named pipe. Из этого подхода родилась общая архитектура подобного рода приложений, названная .NET Pipe RPC. Подход действительно достаточно общий, однако у меня в Ворде величина этой статьи составляет уже 5 страниц, поэтому я заканчваю, и если хабрачеловеки заинтересовались этой темой - отпишите в комментариях, и я подробно расскажу об этом подходе с примерами реализации. Ссылки по теме
|
|