CLR вдоль и поперек

Практические рекомендации по взаимодействию управляемого и машинного кода.

Джесси Каплан (Jesse Kaplan)

Содержание

Когда стоит применять взаимодействие управляемого и машинного кода?
Технологии взаимодействия: Три выбора
Технологии взаимодействия: P/Invoke
Технологии взаимодействия: Взаимодействие COM
Технологии взаимодействия: C++/CLI
Соображения по архитектуре взаимодействия
Структура API и процесс разработки
Производительность и расположение границы взаимодействия
Управление жизненным циклом

В некоторых отношениях, появление статьи вроде этой на страницах журнала MSDN Magazine в начале 2009 года может показаться странным – взаимодействие управляемого и машинного кода поддерживалось в Microsoft .NET Framework, примерно в одной и той же форме, начиная с версии 1.0 в 2002. Вдобавок, можно без труда найти подробную документацию уровня API и средств, а также тысячи страниц подробных документов поддержки. Но чего нет среди всего этого, так это исчерпывающего, высокоуровневого руководства по архитектуре, описывающего, когда использовать взаимодействия, какие архитектурные соображения следует принимать в учет и какую технологию взаимодействия использовать. Это пробел, который я намерен начать заполнять здесь.

Когда стоит применять взаимодействие управляемого и машинного кода?

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

Синтезируя этот опыт, мы сводим вместе три продукта, служащие отличными примерами успешного использования взаимодействия и представительный набор типов использования взаимодействия. Visual Studio Tools для Office – это управляемый набор средств расширения Office и первое приложение, которое приходит мне на ум, когда речь заходит о взаимодействии. Он представляет классическое использование взаимодействия – большое собственное приложение, которое желает включать управляемые расширения или надстройки. Дальше в моем списке находится Windows Media Center, приложение, которое был построено с нуля как смешанно управляемое и машинное приложение. Windows Media Center был разработан с использованием преимущественно управляемого кода, с некоторыми частями (теми, что работают напрямую с селектором каналов и другими драйверами оборудования) написанными в машинном коде. Наконец, у нас есть Expression Design, приложение с уже существующей большой, базой машинного кода, которое желает использовать новые управляемые технологии, в данном случае Windows Presentation Foundation (WPF) для предоставления нового поколения взаимодействия с пользователями.

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

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

Технологии взаимодействия: три выбора.

В .NET Framework доступны три основных технологии взаимодействия и то, какая из них выбрана будет частично определено типом API, используемым для взаимодействия, а частично требованиями и необходимостью контролировать границу. Platform Invoke или P/Invoke – это преимущественно технология взаимодействия управляемый-машинный, которая позволяет вызывать машинные API в стиле С из управляемого кода. Взаимодействие COM – это технология, позволяющая либо использовать машинные интерфейсы COM из управляемого кода, либо экспортировать машинные интерфейсы COM из управляемых API. Наконец, имеется C++/CLI (ранее известная как управляемый C++), которая позволяет создавать сборки, содержащие смесь управляемого и собственного скомпилированного кода C++ и разработана, чтобы служить мостиком между управляемым и машинным кодом.

Технологии взаимодействия: P/Invoke

P/Invoke – это простейшая из трех технологий, которая была разработана в основном для предоставления управляемого доступа к API в стиле С. В случае P/Invoke, каждый API необходимо заключать в оболочку отдельно. Если есть лишь несколько API и их подписи не очень сложны, это может быть очень хорошим выбором. Однако, использовать P/Invoke становится значительно сложнее, если у неуправляемых API имеется много аргументов, лишенных хороших управляемых эквивалентов, таких, как структуры переменной длинны, пустые места *, перекрывающиеся объединения и так далее.

Библиотеки базовых классов (BCL) .NET Framework содержат множество примеров API, которые на деле являются лишь толстыми оболочками вокруг большого числа деклараций P/Invoke. Почти все функции в .NET Framework, заключающие в оболочки неуправляемые API Windows создаются с использованием P/Invoke. На самом деле, даже Windows Forms почти целиком построены на основе машинного ComCtl32.dll, использующего P/Invoke.

Существует несколько очень ценных ресурсов, которые могут сделать использование P/Invoke существенно проще. Во-первых, на веб-сайте pinvoke.net имеется вики-страница, первоначально созданная Адамом Натаном (Adam Nathan) из группы взаимодействия CLR, на которой имеется большое число контролируемых пользователем подписей для широкого набора распространенных API-интерфейсов Windows.

Существует также очень удобная надстройка Visual Studio, делающая несложным обращение к pinvoke.net изнутри Visual Studio. Для API, не охваченных pinvoke.net, происходят ли они из собственной библиотеки разработчика или чьей-то еще, группа взаимодействия выпустила средство создания подписей P/Invoke, именуемое P/Invoke Interop Assistant, которое автоматически создает подписи для машинных API, основываясь на файле заголовка. Прилагающийся снимок экрана показывает средство в действии.

fig01.gif

Создание подписей в P/Invoke Interop Assistant

Технологии взаимодействия: Взаимодействие СОМ

Взаимодействие COM позволяет либо потреблять интерфейсы COM из управляемого кода, либо предоставлять управляемые API как интерфейсы COM. Средство TlbImp можно использовать для создания управляемой библиотеки, предоставляющей управляемые интерфейсы для общения с конкретным файлом tlb. COM. А TlbExp выполняет противоположную задачу и создаст tlb COM с интерфейсами, соответствующими типам ComVisible в управляемой сборке.

Взаимодействие COM может быть очень хорошим решением, если COM уже используется внутри приложения или как его модель расширяемости. Это также простейший способ поддержания точно соответствующей семантики COM между управляемым и машинным кодом. В частности, взаимодействие COM является отличным выбором при взаимодействии с компонентом на основе Visual Basic 6.0, поскольку CLR следует, по сути, тем же правилам COM, что и Visual Basic 6.0.

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

Microsoft Office – наиболее выдающийся пример приложения, использующего взаимодействие COM в качестве мостика между управляемым и машинным кодом. Office был отличным кандидатом для взаимодействия COM, поскольку он давно использовал COM как свой механизм расширяемости и обычно использовался из Visual Basic for Applications (VBA) или Visual Basic 6.0.

Первоначально Office целиком полагался на TlbImp и «тонкую» сборку взаимодействия в качестве своей управляемой объектной модели. Но, с течением времени, продукт Visual Studio Tools for Office (VSTO) был встроен в Visual Studio, предоставляя все более насыщенную модель разработки, которая включала многие из принципов, описанных в этой статье. При использовании продукта VSTO сегодня, порой так же легко забыть, что взаимодействие COM служит основой VSTO, как и забыть, что P/Invoke является основой значительной части библиотек базовых классов.

Технологии взаимодействия: C++/CLI

C++/CLI разработана, чтобы служить мостиком между мирами машинного и управляемого кода и она позволяет компилировать как машинный, так и управляемый код C++ в одну и ту же сборку (даже в один и тот же класс), а также выполнять стандартные вызовы C++ между двумя частями сборки. При использовании C++/CLI, выбирается, какая часть сборки должна быть управляемой, а какая машинной. Получившаяся сборка является смесью языка MSIL (промежуточного языка Майкрософт, имеющегося во всех управляемых сборках) и машинного кода сборки. C++/CLI – технология взаимодействия с очень широкими возможностями, дающая почти полный контроль над границей взаимодействия. Отрицательная сторона состоит в том, что она заставляет брать на себя этот контроль.

C++/CLI может быть хорошим мостиком если необходима проверка статических типов, если строгим требованием является производительность и если необходима более предсказуемая финализация. Если P/Invoke или взаимодействие COM удовлетворяют существующим нуждам, их обычно проще использовать, особенно если разработчики не знакомы с C++.

Есть несколько вещей, о которых необходимо помнить, думая насчет использования C++/CLI. В первую очередь следует запомнить, что если планируется использовать взаимодействие C++/CLI для предоставления более быстрой версии взаимодействия COM, взаимодействие COM работает медленнее чем C++/CLI, поскольку оно выполняет массу работы за разработчика. Если COM используется в приложении достаточно приблизительно и взаимодействия COM с точным соответствием не требуется, то это хороший размен.

Но если используется большая часть спецификации COM, то, вероятно, можно будет обнаружить, что после добавления необходимых частей семантики COM в своей решение C++/CLI COM, несмотря на массу проделанной работы, производительность не лучшей той, что предоставляется взаимодействием. Несколько групп Майкрософт последовали подобным путем, только для того, чтобы осознать это и возвратиться к взаимодействию COM.

Вторым пунктом, который следует держать в уме при обдумывании использования C++/CLI, является то, что эта технология предназначена лишь для роли мостика между управляемым и машинным мирами, а не технологии, используемой для написания основной части приложения. Писать на нем приложения, конечно, возможно, но при этом можно будет обнаружить, что производительность разработчика куда ниже, чем в средах чистого C++ или чистого C#/Visual Basic и что приложение, вдобавок, работает медленнее. Так что при использовании C++/CLI компилируйте лишь нужные файлы с помощью переключения /clr и используйте сочетание чистых управляемых или чистых машинных сборок для создания основных функций приложения.

Соображения по архитектуре взаимодействия

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

Структура API и процесс разработки

Обдумывая свою структуру API, следует задать себе несколько вопросов: Кто будет кодировать для моего уровня взаимодействия и следует ли мне стремиться к облегчению его задачи или к минимизации затрат на создание границы? Будут ли кодировать для этой границы те же разработчики, что пишут машинный код? Есть ли в компании другие разработчики? Являются ли они сторонними разработчиками, расширяющими приложение или использующими его как службу? Каков их уровень подготовки? Уверенно ли они чувствуют себя при работе с машинными парадигмами, или счастливы только при написании управляемого кода?

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

Производительность и расположение границы взаимодействия

Прежде чем тратить слишком много времени на оптимизацию приложения, важно определить, существует ли проблема с производительностью взаимодействия. Многие приложения используют взаимодействие в ключевых для производительности разделах и требуют тщательного рассмотрения этого вопроса. Но многие другие будут использовать взаимодействие в ответ на щелчки мыши пользователя и не столкнутся с проблемой десятков, сотен или даже тысяч переходов взаимодействия, вызывающих задержки. Тем не менее, при рассмотрении производительности решения взаимодействия следует держать в уме две цели: уменьшение числа производимых переходов взаимодействия и уменьшение объема данных, передаваемых пи каждом переходе.

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

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

Управление жизненным циклом

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

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

Когда объем ресурсов не ограничен, можно просто полагаться на вызов сборщиком мусора финализатора объекта и позволение этому финализатору освободить машинные ресурсы (прямо либо непрямо). Когда эти ресурсы ограничены, этот шаблон управляемого избавления может быть очень полезен. Вместо предоставления машинных объектов управляемому коду напрямую, следует поместить вокруг них, как минимум, тонкую оболочку, которая реализует IDisposable и следует стандартному шаблону избавления. Таким образом, если истощение ресурсов составляет проблему, можно прямо избавиться от этих объектов в управляемом коде и высвободить ресурсы, как только работа с ними завершена.

Вторая проблема с управлением жизненным циклом, часто сказывающаяся на приложениях, состоит в том, что разработчики часто видят как неохотную сборку мусора: их использование памяти продолжает расти, но, по какой-то причине, сборщик мусора работает нечасто и объекты остаются живыми. Часто они продолжают добавлять вызовы к GC.Collect, чтобы ускорить решение проблемы.

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

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

Решение этой проблемы состоит в том, чтобы давать сборщику мусора подсказки по реальным затратам памяти на каждую из этих мелких управляемых оболочек для машинных ресурсов. Мы добавили пару API –интерфейсов в .NET Framework 2.0, которые позволяют делать именно это. Тот же тип оболочек, что использовался ранее, можно использовать для добавления шаблонов удаления к недостающим ресурсам, но переделать их для предоставления подсказок сборщику мусора, вместо прямого освобождения ресурсов.

В конструкторе для этого объекта можно просто вызвать метод GC.AddMemoryPressure и передать приблизительную стоимость в машинной памяти машинного объекта. Затем можно вызвать GC.RemoveMemoryPressure в методе финализатора объекта. Эта пара вызовов поможет сборщику мусора понять истинные затраты ресурсов на эти объекты и то, сколько реально памяти освобождается при их освобождении. Отметьте, что важна идеальная балансировка вызовов к Add/RemoveMemoryPressure.

Третье распространенное расхождение между управляемым и машинным мирами в управлении жизненным циклом заключается не столько в управлении отдельными ресурсами или объектами, сколько целыми сборками или библиотеками. Машинные библиотеки могут быть без труда выгружены, когда приложение завершает работать с ними, но управляемые библиотеки не могут быть выгружены самостоятельно. Вместо этого, CLR имеет единицы изоляции, именуемые AppDomains, которые могут быть загружены по отдельности и которые очистят все сборки, объекты и даже потоки, работающие в этом домене, при своей выгрузке. Те, кто создает машинное приложение и привык к выгрузке своих надстроек по завершении работы с ними, найдут, что использование различных AppDomain для каждой из управляемых надстроек даст ту же гибкость, что имелась при выгрузке отдельных машинных библиотек.

Вопросы и комментарии направляйте по адресу clrinout@microsoft.com.

Джесси Каплан (Jesse Kaplan) сейчас работает руководителем программы по взаимодействию управляемого и машинного кода в группе CLR корпорации Майкрософт. В прошлом Джесси занималась вопросами совместимости и взаимодействия управляемого и машинного кода.