CLR с изнанки

Подход In-Process Side-by-Side

Джесс Каплан и Луиз Фернандо Сантос

Опубликуйте свои вопросы и комментарии в блоге группы CLR.

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

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

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

Проблемы надстроек

Упомянутый подход хорош в случае приложений, получающих собственные процессы, но с надстройками дело обстоит гораздо сложнее. Представьте, что вы используете программу вроде Outlook, которая служит хостом для COM-надстроек, в том числе управляемых, и что у вас имеются две версии исполняющей среды, причем каждая надстройка создавалась под разные версии этой среды. Какую же исполняющую среду вы должны выбрать? Загрузка более новой надстройки в более старую исполняющую среду явно не сработает.

С другой стороны, из-за высокого уровня совместимости более старая надстройка обычно нормально работает в более новой исполняющей среде. Чтобы у всех надстроек был максимально высокий шанс на нормальную работу, мы всегда выбираем самую новую исполняющую среду для управляемых COM-надстроек. Даже если у вас установлены только более старые надстройки, у нас нет никакого способа узнать об этом при активации такой надстройки, поэтому загружаться все равно будет более новая исполняющая среда.

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

Для .NET Framework 3.0 и 3.5 мы решили эту проблему, придерживаясь крайне строгой политики: каждый выпуск лишь добавлял новые сборки к предыдущей версии с той же исполняющей средой в основе. Это исключало любые проблемы совместимости при установке новых версий инфраструктуры на компьютерах с .NET Framework 2.0. То есть, когда вы запускаете приложение в .NET Framework 3.5, оно на самом деле работает в исполняющей среде версии 2.0 с несколькими дополнительными сборками поверх нее. Однако это также означает, что мы не можем вводить инновации в сборках .NET 2.0, где сосредоточена основная функциональность, в частности сборщик мусора, JIT-компилятор и библиотеки базовых классов.

В .NET Framework 4 мы реализовали подход, который обеспечивает высокий уровень совместимости, в том числе для существующих надстроек, и в то же время открывает возможность для инновационных изменений в основной части инфраструктуры. Теперь в одном процессе можно одновременно выполнять надстройки как для .NET 2.0, так и для .NET 4. И такой подход мы называем In-Process Side-by-Side, или In-Proc SxS.

In-Proc SxS устраняет большинство распространенных проблем совместимости, но отнюдь не все. В этой статье мы расскажем, почему мы решили создать механизм In-Proc SxS, как он работает и какие проблемы ему не по плечу. Для тех, кто пишет обычные приложения или надстройки, In-Proc SxS «просто работает» — все, что нужно, происходит автоматически. А для тех, кто создает хосты, использующие преимущества In-Proc SxS, мы также поясним обновленный API хостинга и подскажем, как правильно им пользоваться.

Поездка в офис Рея Оззи

В конце 2005 г. почти все топ-менеджеры Microsoft вдруг потеряли возможность проверять почту на своих основных компьютерах. Всякий раз, когда они открывали Outlook, тот без всякой видимой причины аварийно завершался, снова запускался, опять аварийно завершался — и так до бесконечности. Никаких обновлений Outlook перед этим не было. Вскоре источник проблемы был отслежен до управляемого исключения, генерируемого управляемой надстройкой. Один мой приятель («мой» относится к соавтору этой статьи, Джессу Каплану. — Прим. ред.) из группы Visual Studio Tools for Office (VSTO), отвечающий за управляемые надстройки в Office, был отправлен диагностировать эту проблему на компьютере одной из самых известных жертв злополучной ошибки — Рея Оззи (Ray Ozzie), который в то время был главным техническим директором.

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

Мы быстро выяснили, что с ней не так: надстройка вызывала гонки потоков, запуская девять потоков и после запуска каждого из них инициализируя данные, которые обрабатывал поток (рис. 1). Кодировщики изначально удачно подобрали тайминги, но, как только устанавливалась инфраструктура .NET Framework 2.0, надстройка автоматически переключалась на использование .NET 2.0 — по причинам, о которых я недавно рассказывал. Но .NET 2.0 работала немного быстрее при запуске потоков, поэтому скрытая раньше ошибка быстро проявлялась гонками потоков.

Рис. 1. Код из надстройки для Office

Thread [] threads = new Thread[9];
for (int i=0; i<9; i++)
{
    Worker worker = new Worker();
    threads[i] = new ThreadStart(worker.Work);
    threads[i].Start(); //This line starts the thread executing
    worker.identity =i; //This line initializes a value that
                        //the thread needs to run properly
}

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

Нарушение установки

В ходе нашего тестов на совместимость мы наткнулись на одно приложение, которое прекрасно работало, если .NET 2.0 устанавливалась после установки приложения, но сбоило при установке на компьютере, где уже были установлены обе версии — 1.1 (приложение было рассчитано именно на эту версию) и 2.0. Нам потребовалось немало времени, чтобы понять, в чем дело, но в конце концов мы отследили источник проблемы до небольшого блока кода в установщике, выполнение которого опять же автоматически переносилось в среду версии 2.0. Этот блок кода не мог найти каталог инфраструктуры.

Логика поиска этого каталога была весьма «хрупкой» и на самом деле неправильной:

string sFrameworkVersion = System.Environment.Version.ToString();
string sWinFrameworkPath = session.Property["WindowsFolder"] +
"Microsoft.NET\\Framework\\v" +
sFrameworkVersion.Substring(0,8) + "\\";

Но даже после исправления этой ошибки приложение все равно толком не работало. Вот в чем заключалось исправление:

string sWinFrameworkPath = System.Runtime.InteropServices.RuntimeEnvironment.GetRuntimeDirectory();

Оказалось, что установщик искал каталог инфраструктуры для того, чтобы получить путь к caspol.exe и выдать приложению разрешение на выполнение в этой инфраструктуре. Но, найдя нужный путь, установщик выдавал разрешение на выполнение в CLR 2.0, даже если приложение выполнялось в CLR 1.1. Вот этот проблемный код:

System.Diagnostics.Process.Start(sWinFrameworkPath + "caspol.exe " + casPolArgs);

Совместимость через механизм In-Process Side-by-Side

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

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

Увы, ограничение «одна исполняющая среда на один процесс» означало, что в случаях расширения и управляемых COM-компонентов, где несколько независимых приложений выполняется в одном процессе, угодить каждому было просто невозможно. Некоторые компоненты не получали исполняющую среду требуемой им версии, и как мы ни старались сохранить совместимость, какая-то их часть переставала работать.

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

Основополагающие принципы

Чтобы вы лучше понимали некоторые из принятых нами решений, будет полезно обсудить руководящие принципы, которых мы придерживались при проектировании механизма In-Proc SxS.

  1. Установка новой версии .NET Framework не должна влиять на существующие приложения.
  2. Приложения и надстройки должны выполняться в той версии инфраструктуры, на которую они были рассчитаны и в которой они тестировались.
  3. Бывают ситуации, например при использовании библиотек, когда нет возможности запустить код в той инфраструктуре, под которую проектировались эти библиотеки, поэтому мы все равно должны стремиться к 100%-ной обратной совместимости.

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

Помимо этого нам нужно быть уверенными в сравнительной простоте перехода на более новую версию исполняющей среды, поэтому мы удерживали совместимость для .NET Framework 4 на уровне, даже превышающем таковой для .NET 2.0.

Обзор поведения

Исполняющая среда .NET Framework 4 (и все будущие исполняющие среды) способна работать в одном процессе с другой исполняющей средой. Хотя эта функциональность не распространяется на более старые версии исполняющей среды (от 1.0 до 3.5), мы позаботились о том, чтобы версия 4 и более новые могли выполняться в одном процессе с любой другой более старой версией исполняющей среды (только одной). Иначе говоря, вы сможете загружать версии 4, 5 и 2.0 в один и тот же процесс, но не версии 1.1 и 2.0. Инфраструктура .NET Framework версий от 2.0 до 3.5 работает с исполняющей средой версии 2.0, и поэтому конфликтов между ними нет (табл. 2).

Версия .NET Framework        
  1.1 От 2.0 до 3.5 4 5
1.1 Неприменимо Нет Да Да
От 2.0 до 3.5 Нет Неприменимо Да Да
4 Да Да Неприменимо Да
5 Да Да Да Неприменимо

Табл. 1. Удастся ли загрузить эти исполняющие среды в один процесс?

Никакие существующие приложения или компоненты не должны заметить какой-либо разницы после установки исполняющей среды .NET Framework 4: они должны будут по-прежнему использовать ту версию исполняющей среды, под которую их создавали. Приложения и управляемые COM-компоненты, рассчитанные на .NET 4, будут работать в исполняющей среде версии 4. Хосты, которым понадобится взаимодействие с исполняющей средой версии 4, должны будут специально запрашивать такое взаимодействие.

Что In-Process Side-by-Side означает для вас?

Конечные пользователи и системные администраторы Теперь вы можете быть уверены, что при установке новой версии исполняющей среды — независимо от приложения или вместе с ним — она никак не повлияет на ваш компьютер и все существующие приложения будут по-прежнему работать.

Разработчики приложений In-Proc SxS практически ничего не меняет для разработчиков приложений. Приложения по умолчанию всегда используют ту версию инфраструктуры, для которой они были созданы. Единственное изменение, которое может коснуться таких разработчиков, — приложения больше не переключаются автоматически на более новую версию исполняющей среды даже в отсутствие оригинальной для них версии. Вместо этого мы предлагаем скачать оригинальную версию инфраструктуры и предоставляем ссылку на нее, чтобы избавить вас от поисков.

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

Разработчики и пользователи библиотек In-Proc SxS не решает проблемы совместимости, с которыми сталкиваются разработчики библиотек. Любые библиотеки, загружаемые приложением по прямой ссылке или через Assembly.Load*, будут по-прежнему загружаться непосредственно в исполняющую среду и AppDomain соответствующего приложения. То есть, если приложение будет заново скомпилировано под исполняющую среду .NET Framework 4, но сохранит зависимости от сборок, рассчитанных на .NET 2.0, эти сборки тоже будут загружаться в исполняющую среду .NET 4. Поэтому рекомендуется тестировать библиотеки во всех версиях инфраструктуры, которые вы хотели бы поддерживать. Это одна из причин, по которой мы сохраняем высокий уровень обратной совместимости.

Разработчики управляемых COM-компонентов В прошлом эти компоненты автоматически переключались бы на новую исполняющую среду, установленную на компьютере. Теперь компоненты, созданные до .NET Framework 4, по-прежнему будут переключаться на новую исполняющую среду (версии 3.5 или более раннюю), а все более новые компоненты будут загружаться в свою версию исполняющей среды, как показано в табл. 2.

Управляемые COM-компоненты: в какой версии исполняющей среды будет работать мой компонент?        
Версия компонента 1.1 От 2.0 до 3.5 4 5
Состояние компьютера/процесса

Установлены 1.1, 3.5, 4 и 5;

ни одна из них не загружена

3.5 3.5 4 5

Установлены 1.1, 3.5, 4 и 5;

загружены 1,1 и 4

1.1 Загрузка завершится неудачей* 4 5

Установлены 1.1, 3.5, 4 и 5;

загружены 3.5 и 5

3.5 3.5 4 5

Установлены 1.1, 3.5 и 5;

ни одна из них не загружена

3.5 3.5 Загрузка завершится неудачей по умолчанию** 5

* Загрузка этих компонентов завершилась бы неудачей и в прошлом.

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

       

Табл. 2. Взаимодействие управляемых COM-компонентов и разных версий исполняющей среды

Разработчики расширений оболочки Расширение оболочки — обобщенное название самых разнообразных средств расширения оболочки Windows. Два распространенных примера — расширения, которые добавляют команды для контекстного меню файлов и папок, и расширения, которые предоставляют собственные значки или оверлеи значков для файлов и папок.

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

  • расширение оболочки было написано в расчете на исполняющую среду версии N;
  • его нужно загружать в любое приложение на компьютере, в том числе созданные в расчете на исполняющие среды более поздних и более ранних версий, чем N;
  • если приложение ориентировано на более позднюю версию, чем расширение, обычно все работает нормально, если только не возникают проблемы совместимости;
  • если приложение ориентировано на более раннюю версию, чем расширение, сбой гарантирован, поскольку такая версия исполняющей среды, очевидно, не способна выполнять данное расширение оболочки, ориентированное на более новую версию;
  • если бы расширение оболочки каким-то образом удалось загрузить до компонентов приложения с управляемым кодом, его выбор версии инфраструктуры мог бы конфликтовать с тем, что нужно приложению, и это привело бы к краху приложения.

Эти проблемы заставили нас официально рекомендовать воздержаться от разработки (и не поддерживать) внутрипроцессных расширений оболочки, использующих управляемый код. Это был болезненный выбор для нас и наших клиентов, и вы можете сами в этом убедиться на форуме MSDN, где объясняется эта проблема: https://social.msdn.microsoft.com/forums/en-US/netfxbcl/thread/1428326d-7950-42b4-ad94-8e962124043e. Расширения оболочки очень популярны и являются одним из последних мест, где разработчикам определенных типов приложений приходилось писать исключительно неуправляемый код. К сожалению, из-за нашего ограничения, допускающего только одну исполняющую среду на каждый процесс, мы не могли поддерживать их в управляемом коде.

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

Тем не менее разработчики расширений оболочки — управляемых и неуправляемых — могут самостоятельно позаботиться о том, чтобы их расширения могли выполняться в разных средах и корректно работать совместно друг с другом. Ближе к выпуску RTM-версии (release to manufacturing) мы предоставим руководство и примеры, которые помогут вам создавать высококачественные управляемые расширения оболочки, корректно работающие в экосистеме Windows.

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

Если вы пользовались какими-либо API хостинга в .NET Framework до версии 4, то, вероятно, заметили: все они предполагают, что в процесс может быть загружена только одна исполняющая среда. Поэтому, если вы размещаете исполняющую среду, используя наши неуправляемые API, вам понадобится модифицировать свой хост так, чтобы он поддерживал In-Proc-SxS. В связи с новым подходом, позволяющим размещать несколько исполняющих сред в процессе, мы добавили новый API хостинга, который поможет вам управлять несколькими исполняющими средами. Полная документация на этот API будет опубликована в MSDN, но средства этого API сравнительно просты в применении, если у вас есть опыт работы с текущим API хостинга.

Одна из самых интересных задач, стоявших перед нами при разработке In-Proc SxS, заключалась в том, как обновлять поведение существующих API хостинга, поддерживающих только одну исполняющую среду на процесс. Поначалу выбор был довольно широким, но, когда были сформулированы принципы, изложенные ранее в этой статье, нам оставалось лишь одно: API не должен менять свое поведение после установки .NET Framework 4. То есть API-средства хостинга могут распознавать только одну исполняющую среду в каждом процессе и, даже если вы используете их так, что они уже активировали новейшую исполняющую среду на компьютере, они предоставят вам лишь самую последнюю версию младше четвертой.

Однако этот API все же можно «привязать» к исполняющей среде в .NET Framework 4, явно передавая методам этого API номер версии «4» или настроив приложение определенным способом, хотя опять же это требует специально запрашивать CLR 4, а не самую новую исполняющую среду.

Таким образом, код, использующий существующий API хостинга, после установки .NET Framework 4 будет работать по-прежнему, но получит представление процесса, в котором видна лишь одна загруженная исполняющая среда. Кроме того, для сохранения совместимости API-интерфейсы хостинга будут, как правило, взаимодействовать только с версиями младше 4. Более подробная информация о том, какая версия выбирается для каждого из более старых API хостинга, будет доступна в MSDN, но несколько правил, изложенных выше, уже сейчас должны были помочь вам понять, как будет определяться их поведение. Если вам нужно взаимодействие с несколькими исполняющими средами, то придется перейти на новый API хостинга.

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

Из-за такой архитектуры неизбежны ограничения на использование этих сборок в новом мире. Одна из фундаментальных проблем заключается в том, что, если бы мы разрешили многократную загрузку этих сборок в каждый процесс, нам все равно понадобилась бы изоляция между управляемыми и неуправляемыми разделами данных. А это потребовало бы загружать каждый раздел дважды, что не допускается неуправляемым загрузчиком Windows. Полное описание всех причин, по которым мы были вынуждены ввести определенные ограничения, выходит за рамки этой статьи, но обязательно появится на каком-либо ресурсе поближе к выпуску RTM-версии.

Основное ограничение в том, что сборки C++/CLI на основе .NET Framework до версии 2.0 можно загружать только в исполняющей среде .NET 2.0. Если у вас есть библиотека C++/CLI под версию 2.0 и вы хотите использовать ее из версии 4 или более поздней, вам придется перекомпилировать ее с каждой нужной вам версией. Если вы используете одну из таких библиотек, вам надо будет либо получить обновленную версию от разработчика этой библиотеки, либо — как последнее средство — настроить свое приложение на блокирование в его процессе любых исполняющих сред до версии 4.

Заключение

На сегодняшний день Microsoft .NET Framework 4 — выпуск .NET с наиболее высоким уровнем обратной совместимости. Предложив подход In-Proc SxS, Microsoft гарантирует, что простая установка .NET 4 не нарушит работу любого существующего приложения и что все программы, уже установленные на компьютере, будут работать как и раньше.

Конечным пользователям больше не придется беспокоиться о том, что установка инфраструктуры — явная или совместно с каким-либо приложением — навредит какому-то приложению, работающему на их компьютерах.

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

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

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

Джесс Каплан  (Jesse Kaplan) — менеджер программ по направлению «Managed/Native Interoperability» («Взаимодействие между управляемым и неуправляемым кодом») в группе Microsoft CLR. Ранее отвечал за совместимость и расширяемость.

Луиз Фернандо Сантос (Luiz Fernando Santos) — бывший член группы CLR — в настоящее время является менеджером программ в группе SQL Connectivity, где отвечает за управляемые провайдеры ADO.NET, в том числе SqlClient, ODBCClient и OLEDBClient.

Выражаем благодарность за рецензирование данной статьи экспертам Джошуа Гудману (Joshua Goodman), Саймону Холлу (Simon Hall) и Шону Селитренникову (Sean Selitrennikoff)