Немного о сборках

Автор: Алексей Дубовцев
The RSDN Group
Опубликовано: 08.04.2004
Версия текста: 2.0

Немного истории
Первые шаги
Процедурное программирование
Модульное программирование
Прерывания и резидентные программы, как средство интеграции
Динамические библиотеки
Ад динамических библиотек
Способы подключения динамических библиотек
Управление версиями динамических библиотек
Компонентный подход COM
Новое решение
Что же такое сборки
Виды сборок
Статические сборки
Манифест
Идентификатор версии модуля MVID
Модули
«Пустые» сборки
Сборки со строгими именами
Создание строго именованных сборок
Глобальное хранилище сборок (GAC)
Расширение оболочки для управления сборками в GAC
Реальное строение GAC
Инсталляция сборок в GAC
Проверка целостности сборки при инсталляции в GAC
Использование строго именованных сборок
Внутренний формат имён
Политика версий для строго именованных сборок
Информация о версии
Работаем с информацией о версии
Дополнительная информация о версии
Технология прямого запуска
Процесс загрузки .NET приложений
Динамические библиотеки на основе сборок
Локализация приложений при помощи сборок
Конфигурирование политики загрузки сборок
<developmentMode>
<assemblyBinding>
Настройка IIS сервера
Настройка Apache сервера
Проверяем возможность автоматической загрузки сборок из сети
Редактирование конфигурационных файлов при помощи встроенных средств среды исполнения
Процесс поиска сборок средой исполнения
Анализ конфигурационного файла
Проверка ранее загруженных сборок
Поиск в GAC
Поиск файлов сборок в каталоге приложения
Просмотр логов загрузки сборок и исключений при помощи утилиты Fuslogvw.exe
Вот и всё!

Source.zip

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

Акцент сделан на подробном описании внутренних механизмов работы сборок, понимание которых позволит в совершенстве овладеть большинством программных средств, предоставляемыми платформой .NET. Будут рассмотрены как простые темы, вроде создания «строго» именованных сборок, так и сложные вроде низкоуровневого механизма загрузки сборок.

Немного истории

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

Первые шаги

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

Процедурное программирование

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

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

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

Модульное программирование

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

Прерывания и резидентные программы, как средство интеграции

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

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

Последний пункт стал, пожалуй, основным препятствием к повсеместному использованию прерываний, в качестве основной технологии распределения программного кода. Хотя было бы крайне неверно говорить, что прерывания все же не получили распространения. Во времена MS-DOS они были, пожалуй, единственным и наиболее популярным способом предоставления общедоступных программных сервисов. Настоящая технология не забыта и сегодня, она с успехом применяется в недрах операционных систем. К примеру, в ОС Microsoft Windows 2000 все вызовы ядра проходят через программное прерывание под номером 2Eh, хотя для программистов высокого уровня, использующих обычное Windows API, это не является явным. Рассматривая функцию API ReadFile, можно с лёгкостью показать, что её вызов на определённом этапе приводит к обращению к прерыванию 2Eh с параметром 0A1h, передаваемым в регистре eax. Где прерывание 2Eh является стандартным шлюзом для подавляющего большинства API вызовов на уровень ядра, а параметр 0A1h собственно и определяет искомую функцию, в данном случае ReadFile.

ПРИМЕЧАНИЕ

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

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

Динамические библиотеки

Следующим шагом на пути распределения программного кода, явилось создание динамически подключаемых библиотек, наверняка знакомых читателю под широко известной аббревиатурой DLL (Dynamic Link Library). С появлением данной технологии стало возможным создание, внешних полностью самостоятельных библиотек. Обращение к коду, которых могло наглядно осуществляться из любых языков и средств разработки.

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

ПРИМЕЧАНИЕ

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

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

Сама библиотека содержит откомпилированный процессорный код, а так же ряд служебных таблиц описывающих содержащиеся в ней функции. По запросу программы или, что более вероятно, загрузчика исполняемых файлов Windows, код библиотеки копируется в её адресное пространство, становясь с этого момента доступным для прямого вызова. Данный вызов может быть осуществлен непосредственно при помощи инструкции call, jmp или прямого изменения регистра IP (здесь и далее будет рассматриваться только программы ориентированные на архитектуру x86). В большинстве же случаев вызовы происходят, как обычный вызов функции, в привычной для языка лексике, а о низкоуровневых подробностях заботиться сам компилятор.

Процесс загрузки динамической библиотеки схематично изображен на рисунке 1.


Рисунок 1

Для максимального упрощения, описания механизма работы динамических библиотек, здесь ничего не было сказано, о переадресации функций, таблицах импорта и экспорта, проецировании кода, пересчёта смещений, а так же о многом другом, но в общих чертах все осталось верно. Для получения более полной информации о работе динамических библиотек, стоит обратиться к прекрасной книге Джефри Рихтера (Jeffrey Richter): "Programming Application for Microsoft Windows" или же напрямую к документации Microsoft Platform SDK.

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

ПРИМЕЧАНИЕ

Форматом вызова функции называется соглашение о способе передачи её параметров и возвращении результата её работы. Всего существует семь. форматов вызовов: stdcall, cdecl, fastcall, thiscall, PASCAL, FORTRAN, SYSCALL. Причем последние три считаются безнадёжно устаревшими и более не поддерживаются.

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

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

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

Ад динамических библиотек

Помимо возможных неприятностей с вызовами, динамические библиотеки принесли в жизнь программистов куда более серьёзную проблему, которую окрестили адом динамических библиотек (DLL Hell). Для того чтобы пояснить суть настоящей проблемы, необходимо в общих чертах обрисовать механизм подключения динамических библиотек. Если читатель чувствует себя осведомлённым в данной области он может смело пропустить следующие пояснения.

Способы подключения динамических библиотек

Всего существует два способа подключения динамических библиотек: статический и динамический. Наиболее распространён статический тип подключения. Он подразумевает связывание с библиотекой во время сборки (компиляции) приложения. Делается это при помощи добавления записи в таблицу импорта исполняемого файла. Данная запись представляет собой имя файла библиотеки, а так же список используемых из неё функций. Перед началом исполнения программы загрузчик Windows проверяет записи в таблице импорта и автоматически подгружает необходимые библиотеки, а так же при помощи специального механизма связывает указанные фукнкции со ссылками внутри исполняемого файла. Таким образом, загрузка библиотек происходит автоматически, без явного участия кода программы.

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

HMODULE LoadLibrary(
 LPCTSTR lpFileName // Имя файла содержащего динамическую библиотеку
);

,расположенной в kernel32.dll, которая в свою очередь должна быть подключена к программе статически.

ПРИМЕЧАНИЕ

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

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

FARPROC GetProcAddress(
 HMODULE hModule,  // Дескриптор загруженной библиотеки
 LPCSTR lpProcName // Имя функции
);

Передав ей в качестве первого параметра описатель библиотеки загруженной при помощи функции LoadLibrary, а в качестве второго имя необходимой нам функции или её же порядковый номер. В результате мы получим адрес данной функции, внутри адресного пространства нашего процесса (ведь библиотека с функциями уже загружена внутрь). После чего мы можем осуществлять вызов данной функции любыми доступными нам средствами, начиная от обычного вызова функции в стиле нашего языка и заканчивая прямым вызовом команды call по данному адресу.

Если на секунду взглянуть на оба способа подключения библиотек со стороны, то становиться ясно, что они в принципе идентичны, как бы это заявление и не казалось странным. Поскольку в обоих случаях, для загрузки библиотеки, используется одна и та же функция LoadLibrary. Только в первом случае её вызывает загрузчик исполняемых файлов Windows, а во втором сама программа.

Теперь, когда мы полностью знакомы с механизмом загрузки динамических библиотек, можно перейти к детальному рассмотрению проблемы окрещенной адом динамических библиотек (Dll Hell).

Как нетрудно заметить, для загрузки динамической библиотеки используется лишь имя файла, в которой она расположена. Ранее Microsoft рекомендовала хранить все общие динамические библиотеки в едином хранилище – каталоге system для систем класса 9x и каталоге system32 для систем класса NT.

ПРИМЕЧАНИЕ

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

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


Рисунок 2

Оно сообщает пользователю о том, что в библиотеке Some.dll не была найдена функция hello. При этом прошу обратить внимание на то, что сама библиотека была успешно найдена и загружена. В случае если загрузчик исполняемых файлов обнаружит несоответствие библиотек и выдаст подобное сообщение об ошибке, то можно считать, что вы обошлись малой кровью. Все может быть намного хуже. К примеру, если будет загружена та библиотека которую вроде бы хотелось, но только другой версии. А поскольку в библиотеке нет никакой информации о прототипах функций, и проверить правильность передаваемых в функцию параметров нет никакой возможности, то скорее всего при вызове её функций возникнет исключительная ситуация.

ПРИМЕЧАНИЕ

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

А если уж это действительно произойдет, то можно со сто процентной вероятностью говорить о том, что приложение аварийно завершит свою работу.

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

Позднее Microsoft разобралась, что гораздо безопаснее хранить библиотеки совместно с приложениями, и жертвы дисковым пространством вполне оправданы перед лицом данной серьезной проблемы. Она (Microsoft), конечно же, рекомендовала хранить библиотеки совместно с приложениями. Но, к сожалению, огромное количество программистов не только не знакомо с данной рекомендацией, но и вообще ничего не слышали о проблеме ада динамических библиотек и до сих пор размещают свои творения прямо в каталоге system32 (system).

Управление версиями динамических библиотек

Осознав всю серьёзность проблемы, Microsoft разработала механизм управления версиями динамических библиотек. Сделано это было при помощи введения дополнительного ресурса VERSIONINFO, который содержал информацию о версии, а так же специального Versioning API для управления им. Функции данного, представлены ниже:

GetFileVersionInfo
GetFileVersionInfoSize
VerFindFile
VerInstallFile
VerLanguageName
VerQueryValue

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

Для поддержки старых приложений приходится в новых библиотеках оставлять даже морально устаревшие функции. Так, к примеру, в псевдо ядре Windows - библиотеке kernel32.dll, существует множество функций, которые не используются современными приложениями, но оставлены из-за необходимости поддержки совместимости со старыми.

ПРИМЕЧАНИЕ

Вы сможете найти список всех устаревших функций в Platform SDK на странице Obsolete Windows Programming Elements.

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

Компонентный подход COM

Следующим этапом стало появление компонентной объектной модели (COM – Component Object Model).Данная технология впервые была реализована в составе Windows 95.

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

ПРИМЕЧАНИЕ

GUID - globally unique identifier – глобально уникальный идентификатор. Это 128-битное (16-байтное) число которое создается при помощи специального алгоритма гарантирующего его полную уникальность. Данный алгоритм учитывает: время (с точностью до нано секунд) и географическое положение (номер сетевой карты). Такой хитрый способ генерации GUID позволяет свести практически до нуля вероятность возникновения одинаковых идентификаторов. Правда, нельзя не упомянуть, о том, что прецеденты совпадения идентификаторов уже наблюдались, но это были единичные случаи и по этому поводу не стоит беспокоиться.

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

ПРИМЕЧАНИЕ

Основная база данных COM находится в реестре, в разделе HKEY_CLASSES_ROOT. А записи GUID идентификаторов, хранятся ниже в подключе HKEY_CLASSES_ROOT\Clsid. А так же дополнительную информацию здесь: HKCR\Interface, HKCR\TypeLib.

Для усиления политики управления версиями идентификаторы были введены не только на уровне имён самих компонент, но так же и на уровне самих функций экспортируемых ими. Правда, теперь компоненты экспортировали не отдельные функции, а целые их группы, объединённые в интерфейсы. Каждому такому интерфейсу сопоставлялся уникальный 128 битный GUID идентификатор, который исключал возможность случайного использования другого. Казалось бы, такая мощная система разделения кода и управлениями версиями была неуязвима, но как показало время она была не так уж и надёжна. Проблема пришла сразу с двух сторон. Во-первых, технология COM, все же базировалась на динамических библиотеках, в которых помещались сами компоненты. А с библиотеками зачастую происходила путаница, связанная с совпадением имён файлов в которых они расположены. Но это была еще не самая страшная проблема. Основная неприятность крылась в базе данных COM, расположенной в реестре. Работа с данной базой предполагалась напрямую, без использования специального дополнительного API, которое обязательно следовало бы ввести, чего Microsoft к сожалению не сделала. А поскольку раздел реестра, в котором храниться база COM, является общедоступным, то он после продолжительной эксплуатации Windows, в большинстве случаев приходил в нерабочее состояние. С данной ситуацией наверняка знакомы большинство из пользователей – когда после установки значительного количества программ на компьютер он приходил в не работоспособное состояние, проявляющее себя в очень странных конфликтах.

Новое решение

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

В основе новой технологии лежат сборки (assembly), которые являются наименьшими строительными блоками .NET, призванными обеспечить безопасное разделение кода в приложениях .NET. Далее вы узнаете о сборках все необходимое и сокровенное.

Что же такое сборки

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

Далее мы рассмотрим каждый из пунктов более подробно.

Виды сборок

Сборки разделяются на два вида: статические и динамические. Статические сборки располагаются на диске в файлах формата PE (Portable Executable) и могут содержать заранее откомпилированный IL код, а так же дополнительные ресурсы. Динамические сборки располагаются прямо в памяти и не имеют дискового файла. Хотя, они вполне могут быть сохранены на диск после исполнения. Такие сборки создаются налету (во время исполнения программы) при помощи специальных средств общей библиотеки классов (FCL), названных технологией отражения (reflection).

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

Статические сборки

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


Рисунок 3

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

ПРИМЕЧАНИЕ

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

С первого взгляда Microsoft поступила более чем странно, использовав для хранения сборок файлы формата PE и тем самым, казалось бы, ограничив переносимость своих приложений классом операционных систем Windows. Но такое странное решение, оказывается весьма продумано и оправдано. На самом деле использование формата PE конечно же нисколько не сужает круг платформ и переносимость приложений .NET. Дело в том, что сборки загружаются и исполняются под управлением виртуальной машины .NET, а следовательно формат файлов никакого значения не имеет. В чем можно прекрасно убедиться на примере Java.

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

Теперь мы перейдем к рассмотрению самих файлов статических сборок, тех которые вложены внутрь PE файлов. Они включают в себя четыре основных элемента:

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

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

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


Рисунок 4

Так же под крышей одной сборки можно объединить несколько файлов, в которые поместить различные элементы сборки. Пример такой сборки показан на рисунке 5.


Рисунок 5

Как видно из рисунка за пределы сборки можно выносить не только ресурсы, но даже и код, который будет представлен в виде модулей.

У многофайловых сборок есть три крайне сильных преимущества перед однофайловыми.

  1. В состав одной сборки можно включить модули, код которых разработан на различных языках программирования. Более подробно вы узнаете об этом в разделе про модули далее в данной статье.
  2. При запуске приложения через сеть, очень удобно поместить код сборки и ресурсы в разные файлы. Они будут подгружаться не все сразу, а каждый отдельно по мере непосредственного обращения к ним, что существенно позволит снизить трафик при первоначальной загрузке приложения.
  3. Крайне удобно выносить ресурсы за пределы сборки, по крайней мере, во время отладки и разработки приложения, поскольку для их редактирования не придётся прибегать к специальным средствам извлекающих их из сборок. Они (ресурсы) будут доступных в виде обычных дисковых файлов, которые можно будет редактировать привычным образом.

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

Манифест

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

Информацию о манифесте можно просмотреть используя IL листинг данной сборки (он может быть получен при помощи утилиты ildasm.exe, с параметром /out:имяфайла.il).

Он обычно располагается в самом начале кода. Так же вы можете использовать утилиту ildasm.exe в интерактивном режиме. В этом случае, для того, чтобы просмотреть манифест необходимо открыть ветку MANIFEST в главном окне утилиты.


Рисунок 6

Приведём пример манифеста.

      //  Microsoft (R) .NET Framework IL Disassembler.  Version 1.1.4322.573
      //  Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.
      .assembly
      extern mscorlib
{
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )             // .z\V.4...ver 1:0:5000:0
}
.assembly Some
{
  // --- The following custom attribute is added automatically, do not uncomment -------//  .custom instance void [mscorlib]System.Diagnostics.DebuggableAttribute::.ctor(bool,//                                                                                bool) = ( 01 00 00 01 00 00 ) .hashalgorithm 0x00008004
  .ver 0:0:0:0
}
.module Some.exe
.moduleextern Some2.netmodule

// MVID: {AE5CD969-464A-4D97-93AB-AFF02E963A76}.imagebase 0x00400000
.subsystem 0x00000003
.filealignment 512
.corflags 0x00000001
.mresourcepublic prc.nlp
{
  .file prc.nlp
}

Каждый из элементов подробно описан в таблице 1.

Директива IL Описание
.assembly extern <имя сборки> Указывает внешнюю сборку необходимую для работы данной. (В нашем случае это ссылка на FCL (Framework Class Library – общую библиотеку классов), которая преимущественно располагается в сборке mscorlib)
.publickeytoken <хеш> Определяет маркер открытого ключа подключаемой сборки. Необходим для точной идентификации сборки.
.ver <версия> Определяет необходимую версию подключаемой сборки
.culture Указывает региональную принадлежность сборки
.assembly <имя сборки> Указывает имя сборки
.hash algorithm Определяет алгоритм по которому рассчитывался маркер открытого ключа данной сборки.
.ver 0:0:0:0 Определяет версию сборки.
.public Открытый ключ, встраиваемый в сборку
.culture Указывает региональную принадлежность сборки
.module <имя файла> Указывает имя основного модуля данной сборки.
.module extern <имя файла> Указывает имя внешнего модуля входящих в данную сборку, на которые есть ссылки в коде данной сборки (модуля)
.imagebase <адрес> Указывает адрес, по которому следует загружать данную сборку а адресное пространство приложения.
.subsystem <константа> Указывает на тип приложения. В нашем случае число 3 указывает на то, что приложение консольное.
.file alignment <512> Определяет выравнивание, использовавшееся компилятором при создании данного файла.
.corflags <константа> На данное время данная директива является зарезервированной.
.mresource public <название ресурса> Указывает на то, что к сборке будет подключен общедоступный ресурс с заданным именем.
.file <имя файла> Определяет имя файла в котором располагается ресурс.
.file <имя файла> Подключает внешний модуль, на который при этом может не быть явных ссылок в коде данной сборки.
.entrypoint Указывает на то, что точка входа в приложение будет располагаться в данном модуле.
Таблица 1

Все эти элементы вкупе называются «метаданными сборки». Такое имя они получили за то, что описывают сборку. А в рамках проекта .NET информация, описывающая что-либо называется метаданными. Все перечисленные элементы могут быть заданы самим программистом, при помощи специальных программных атрибутов, ключей компилятора (настроек среды IDE Visual Studio) или же специальных утилит командной строки.

Наблюдательные читатели наверняка заметили, что в таблицу не попал, один элемент манифеста: MVID. Он даже был закомментирован в тексте IL кода. Данный элемент достаточно важен и заслуживает того, чтобы рассказать о нём отдельно.

Идентификатор версии модуля MVID

MVID расшифровывается как Module Version Identifier – Идентификатор версии модуля. Это 128 битный уникальный идентификатор, который автоматически генерируется при каждом построении сборки (модуля) и автоматически добавляется в манифест. Данный идентификатор используется только во внутренних целях среды исполнения. Сами программисты с ним не сталкиваются. Именно поэтому он и был закомментирован, в манифесте. Поскольку он генерируется автоматически, то в языке IL не предусмотрено директивы для его указания.

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

Модули

Модули в среде .NET используются только для хранения кода приложений и, причем, только они. Сие означает, что код может храниться только в модулях, и нигде более. «Но как же так?» - воскликнет внимательный читатель – «в предыдущем же разделе говорилось, что код приложений может храниться в сборках». В большинстве случаев приложения распространяются в виде однофайловых сборок, которые неявно содержат модуль, в котором, как раз и располагается код. Но вполне реальна ситуация, когда сборка не будет иметь вложенного модуля, а соответственно и ни байта кода внутри себя. Фактически сборки используются в среде .NET лишь для объединения файлов в единые сущности, которыми более удобно управлять.

ПРЕДУПРЕЖДЕНИЕ

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

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

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

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

        /*
    Листинг 1
    File:   Module.cs
    Author: Dubovcev Aleksey
*/
        // Подключаем основное пространство имён общей библиотеки классов
        using System;

// Единственный класс нашего модуляclass Mod
{
    // Метод, демонстрирующий работу модуляpublicstaticvoid DoIt()
    {
        // Выведем на консоль приветствие
        Console.WriteLine("Hello World from Module");
    }
};

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

ПРИМЕЧАНИЕ

Сразу же хочу сказать, что скомпилировать данный листинг в модуль при помощи IDE Visual Studio .NET, вам не удастся, по той простой причине, что она не поддерживает работу с модулями и многофайловыми сборками.

Для того чтобы превратить данный листинг в модуль нам придётся воспользоваться компилятором командной строки csc, вызвав его при помощи следующей команды.

csc /t:module Mod.cs

Где ключ /t:module указывает на то, что из исходного кода расположенного в фале Mod.cs необходимо создать модуль.

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

        /*
    Листинг 2
    File:   Some.cs
    Author: Dubovcev Aleksey
*/
        // Подключаем основное пространство имён общей библиотеки классов
        using System;

// Основной класс нашего приложенияclass App
{
    // Точка входа в приложениеpublicstaticvoid Main()
    {
        // Сообщим пользователю о начале работы нашего приложения
        Console.WriteLine("Enter to main application");
        // Проверим модуль
        TestModule();
    }

    // Здесь состоится обращение к коду модуляpublicstaticvoid TestModule()
    {
        // А здесь мы используем только что созданный нами модуль,// вызовем его единственную функцию
        Mod.DoIt();
    }

};

Использование кода внешних модулей настолько просто, что в данном случае может даже показаться, что класс Mod входит в пространство System общей библиотеки классов среды .NET. Обращение к коду внешних модулей входящих в данную сборку не требует использования специальных средств и является абсолютно прозрачным для программиста.

Для того, чтобы скомпилировать данный код в сборку необходимо воспользоваться компилятором csc, которому указать на необходимость подключения ранее созданного модуля, при помощи ключа /addmodule.

csc /addmodule:Mod.netmodule Some.cs

В итоге должно получиться два файла: Some.exe (файл сборки) и Mod.netmodule (внешний модуль). Фактически, в только что созданном, приложении присутствует два модуля, хотя это и не является явным. Первый входит в состав файла Some.exe и является основным модулем приложения, поскольку содержит точку входа. А второй модуль, как не трудно догадаться, внешний и располагается в файле Mod.netmodule.

ПРИМЕЧАНИЕ

Для того, чтобы посмотреть, как подключается внешний модуль к сборке, вы можете просмотреть её манифест - одним из способов описанных в предыдущем разделе. В нём вы обязательно найдёте директиву .module extern Mod.netmodule, которая и отвечает за его подкючение.

В результате работы исполняемого файла приложения (Some.exe) на консоль будут выведены следующие строки.

Enter to main application
Hello World from Module

После запуска приложения управление передаётся функции Main, первая строка которой выводит на консоль сообщение, информирующее о запуске приложения. Код компилируемый из данной строки располагается в основном модуле приложения, а следовательно он будет доступен всегда и выполниться в любом случае. Далее произойдёт вызов функции TestModule, которая уже будет непосредственно обращаться к коду модуля. Перед исполнением данной функции, она будет передана JIT-компилятору (Just-In-Time compiler – компилятор времени исполнения), который откомпилирует её из .NET байт кода в процессорный (машинный). При предварительном сканировании кода метода TestModule, JIT-компилятор обнаружит, что функция обращается к коду внешнего модуля – Mod.netmodule. После чего JIT-компилятор попытается его подгрузить. Таким образом, модули загружаются непосредственно перед, обращением к их коду. Соответственно если приложение ни разу не обратиться к коду внешнего модуля, он загружен не будет, хотя остальной код программы будет выполнен. Это можно легко проверить, удалив файл Mod.netmodule из каталога нашего приложения. На консоль будет выведена лишь первая строка, после чего среда исполнения сообщит о том, что необходимый для дальнейшего исполнения, модуль, найден не был.

Enter to main application

Unhandled Exception: System.IO.FileNotFoundException: File or assembly name Mod.netmodule, or one of its dependencies, was not f
ound.
File name: "Mod.netmodule"
   at App.TestModule()
   at App.Main()

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

У модулей есть одно очень полезное и при этом не документированное свойство. Они одновременно могут входить в состав нескольких сборок. То есть можно создать модуль поместив в него общий для нескольких сборок участок кода, а затем подключить его ко всех этим сборкам (рисунок 7).


Рисунок 7

Это потрясающе удобно, поскольку, изменяя код только одного модуля, мы сразу же подвергаем изменениям все зависимые от него сборки. Скептики могут, конечно же, сказать, что, то же самое можно сделать при помощи подключения дополнительных сборок. Но использование в данных целях модулей имеет существенное преимущество перед сборками. Код помещённый в модули будет, входит в состав сборки, которая его подключает, а следовательно будет иметь общие с ней атрибуты безопасности и конфигурационные настройки. Таким образом, код модуля Some.netmodule будет автоматически иметь атрибуты и привилегии той сборки, в состав которой он входит, а в случае помещения общего кода в отдельные сборки данные параметры придётся настраивать вручную. В итоге можно заключить, что обобществление кода между приложениями при помощи модулей гораздо более удобно, чем при помощи сборок.

«Пустые» сборки

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

К сожалению, обычными средствами создать ни «пустую» сборку, ни модуль, который мы будем при помощи неё подключать, не удастся. Поэтому нам придётся воспользоваться низкоуровневым языком IL.

Первым делом мы создадим модуль, который подключим к пустой сборке. Он будет кардинально отличаться от обычных тем, что в нём будет располагаться точка входа нашего приложения. Да, именно так, я не оговорился! Я даже хочу специально обратить внимание на это. Точка входа в приложение будет располагаться не в основном исполняемом файле, а во внешнем модуле. Его код приведён ниже.

        /*
    Листнг 4
    File:   Module.il
    Author: Dubovcev Aleksey
*/
        // Устанавливаем связь с общей библиотекой классов FCL
        // она понадобиться нам для вывода текста на консоль

.assembly extern mscorlib {}

// Определим имя нашего модуля

.module Module.mod

// Это точка входа в нашу будущую программу// она будет располагаться в данном модуле
.method publicstaticvoid  EntryFunction()
{
    // Указываем на то, что данная функция будет точкой входа// в приложение
  .entrypoint

    // Выделим в стеке одну ячейку. Позднее мы туда загрузим// ссылку на строку, которую выведем на консоль
  .maxstack  1

    // Помещаем в стек ссылку на строку,// которая будет передана в качестве параметра функции,// вызываемой далее
    ldstr      "Hello World from module"// Выведем строку на консоль
    call       void [mscorlib]System.Console::WriteLine(string)

    //выходим из функции
    ret

}   //Вот собственно и всё

Отдельно хотелось бы обратить внимание уважаемого читателя, на то, что имя функции – точки входа в приложение может быть произвольным, а не только Main. Главное, чтобы у данной функции был указан атрибут .entrypoint. Кроме этого момента код модуля тривиален, и не содержит ничего сверхъестественного. Для того, чтобы скомпилировать данный исходный текст необходимо воспользоваться компилятором IL кода (ilasm.exe) со следующими параметрами.

ilasm Module.il /DLL /out=Module.mod

В результате должен получиться модуль в виде файла под именем Module.mod.

Теперь нам предстоит создать ту самую «пустую» сборку, которая будет использовать данный модуль. Код данной сборки мы напишем так же на IL, поскольку только в этом языке присутствует возможность непосредственной работы с манифестом. А поскольку данная сборка будет представлять собой, один лишь манифест, то язык IL окажется незаменим. Код «пустой» сборки представлен ниже.

        /*
    Листинг 5
    File:   Some.il
    Author: Dubovcev Aleksey
*/
        // Определяем имя нашей сборки
.assembly Some {}

// Подключаем внешний модуль
.file Module.mod
    // Указываем компилятору, на то что точка входа в наше приложение// будет располагаться в данном модуле
    .entrypoint

Из данного исходного кода необходимо создать сборку, сделать это можно при помощи компилятора ilasm, используя следующую простую командную строку.

ilasm Some.il

Дополнительных параметров, кроме имени файла указывать не надо. Результатом компиляции данной программы должен явиться файл Some.exe. Это «пустая» сборка являющаяся исполняемым файлом созданного нами приложения. В результате её запуска на консоль будет выведена строка.

Hello World from module

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


Рисунок 8

Как будет рассказано далее, _CorExeMain является внутренней точкой входа в любое приложение .NET.

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

Сборки со строгими именами

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

В совместном режиме сборки помещаются в глобальное хранилище (GAC) из которого они доступны любым приложениям. Здесь уже будет недостаточно одного имени для связывания со сборкой, необходим более устойчивый алгоритм связывания, который бы гарантировал уникальность и точность связывания. С этой целью для сборок были введены строгие имена. Помимо обычного символьного имени, версии и региональной информации строгое имя требует подписание сборки открытым криптографическим ключом, что гарантирует полную уникальность её имени (идентификации).

ПРИМЕЧАНИЕ

Обратите внимание на то, что сборка содержит открытый ключ. А строгое имя маркер открытого ключа. Это два разных понятия, которые будут раскрыты далее.

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

Some, Version=1.2.0.0, Culture=neutral, PublicKeyToken=672eb6fe2413d741
Some, Version=1.2.0.0, Culture=ru-RU, PublicKeyToken=672eb6fe2413d741
Some, Version=1.2.0.0, Culture=ru-RU, PublicKeyToken=324cf587987a87dc

Обратите внимание на то, что при совпадении символьных имён, полные имена не совпадают, из-за различия регионального идентификатора, а так же маркера открытого ключа.

ПРИМЕЧАНИЕ

Маркер открытого ключа является 64 битным (8 байтным) числом, которое является хешем (контрольной суммой) открытого ключа. Маркер был введён для того, чтобы сократить запись строгого имени сборки. С маркером ключа работать гораздо удобнее, поскольку он намного меньше, чем сам открытый ключ.

Правда, у вас могут возникнуть сомнения по поводу надёжности маркера по сравнению с реальным ключом. Можете не беспокоиться, он (маркер) по умолчанию рассчитывается при помощи алгоритма SHA-1, которого с лихвой хватает для обычных приложений.

Если вы хотите подробнее ознакомиться с теорией открытых ключей, хешами, а так же другими криптографическими алгоритмами, то вам стоит ознакомиться с книгой Брюса Шнайдера – «Прикладная криптография».

В самой сборке помещен, конечно, не маркер, а сам открытый ключ. А вот ссылка на сборку со строгим именем производится по маркеру открытого ключа. Убедиться в этом можно, обратившись к манифесту любого файла использующего сборку со строгим именем. Прекрасным примером такой сборки является mscorlib – основной сборки FCL (общей библиотеки классов). Запись о связи с ней присутствует в манифесте практически любой сборки и выгладит так.

.assembly extern mscorlib
{
    // Это маркер открытого ключа сборки
  .publickeytoken = (B7 7A 5C 56 19 34 E0 89 )             // .z\V.4..
  .ver 1:0:5000:0
}

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

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

ПРИМЕЧАНИЕ

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

Такой подход полностью исключает возможность использования чужой сборки, вместо необходимой. Если конечно разные создатели сборок, не будут пользоваться одной и той же парой ключей при создании сборки. Случайное создание идентичной пары ключей полностью исключено!

Помимо участия в строгом имени сборки, открытый ключ используется для её подписания. Происходит это так. При создании сборки компилятор рассчитывает контрольные суммы всех файлов входящих в сборку, затем формирует базовый файл сборки, в манифесте которого будут указаны данные значения и полный открытый ключ сборки. Поле чего считает контрольную сумму базового файла итоговой сборки, которую при помощи пары ключей (открытого и закрытого) преобразует в цифровую подпись RSA. Данная подпись помещается в специальную секцию PE файла, не участвующую при подсчёте общей контрольной суммы, что позволяет провести процесс в обратном порядке. То есть подсчитать контрольную сумму сборки, и при помощи сохранённой цифровой подписи и открытого ключа, проверить целостность файла.


Рисунок 9

Данный механизм позволяет предотвратить несанкционированное изменение сборки – её кода, или любых входящих в её состав файлов.

Создание строго именованных сборок

Первым делом нам предстоит создать пару криптографических ключей, которыми мы будем подписывать нашу сборку. Ключи создаются при помощи утилиты sn.exe, с использованием параметра –k.

sn –k keys.snk

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

sn -tp keys.snk

Microsoft (R) .NET Framework Strong Name Utility  Version 1.1.4322.573
Copyright (C) Microsoft Corporation 1998-2002. All rights reserved.

Public key is
0702000000240000525341320004000001000100834b28783aa71a3b1f6188a895c51051989aca
bf8a340953dca9d3e1ff6e84573ad49cfc3f4949cf6deb0e1cc6c907e5fad12f245eaef3a1d026
1ea78e8ea15095d256f700031d063316a428d05182874924a31123b8f536264c4924b3f17c6471
44ed9a13c149fd5bdb90c1cfaa175f098c38a5d9beb7af662578b76c8d69d7c309a0de521db83a
cd547a432ddc8c8d3ab0140352761e7321f39cdb43e79e5fff2337bf5041dcfb5d0aeca4db7241
b00346bbd161603fa5759c5c84d00a0df441db6453c5fce7878b7a911135345d687fa6361b4bac
a491d879223ad1804bfca7908cee00eeb9c190c3d631368be9a3b90e35cd01c160485fea3e2805
8df5e101697c6e65ad1ca575e2a183a93a57644f8df5d3671f6ac2726a2c8ee5079e704714dabe
ebbb36d49b4f59debdb9eb82de515fed1c285efdf84f1fc6794753bbc17b46c58eb7c7ff9436ca
3d9ccb00bfe93bff46b80b589a7e39012e96fd71f8e60e2ca94e8918e9ee53887e75991c1bdfc0
171ad759c39b9f11e96ce17c1e722a13c26967ae9caefb8f913a3f8fabb18f3585b2396de5dc76
3594f027e3915d7ee8a0779226c22dfc59261b2d54b1195def8a76be314748698146841cb76c3b
0193b3aeede8ae48bdb4d6abab0d64d87ae1723f043fa90411c637189e54e323cfcc536b7d3aa4
9344858efe293b29255e4961b2b633e711f74e9af2f2b1189c2857e8b311d569e263bc6a58ef3c
ac91c6a0c914d2adcfdbf0bf8d5fe2b1526ba1411fcd72b3619f084ae2348e4d0ec3fc224a6185
213d6c92d36e5b4e01c844

Public key token is 60582cc2df93bf17 

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

ПРЕДУПРЕЖДЕНИЕ

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

«Хранить в прохладном и темном месте, не доступном для детей!»

Теперь, располагая парой ключей, мы можем перейти к созданию строго именованной сборки. Для этого, при помощи атрибута AssemblyKeyFile, укажем компилятору, что сборку необходимо подписать при помощи пары ключей, располагающихся в файле Keys.snk. В исходном коде применение данного атрибута будет выглядеть так:

        // В общем виде
[assembly: AssemblyKeyFile("Имя файла с парой ключей")]
// В нашем случае
[assembly: AssemblyKeyFile("Keys.snk")]

При автоматическом создании проекта в среде разработки VisualStudio .NET данный атрибут, располагается в файле AssemblyInfo.cs. Хотя на самом деле, конечно же, принципиального значения, местоположение данного атрибута, не имеет.

Код нашей строго именованной сборки представлен ниже.

        /*
    Листинг 7
    File:   Strong.cs
    Author: Dubovcev Aleksey
*/
        // Подключаем основное пространство имён общей библиотеки классов
        using System;

// В данном пространстве имён находиться атрибут AssemblyKeyFile,// используемый, для указания имени файла с криптографическими ключамиusing System.Reflection;

// В данном пространстве имён находиться класс MessageBox, позволяющий// вывести на экран диалоговое окноusing System.Windows.Forms;

// Зададим имя файла с парой криптографических ключей, которые будут// использованы для подписания данной сборки
[assembly: AssemblyKeyFile("TeskKey.snk")]

// Данное пространство имён отделит типы нашей сборки от остальныхnamespace nmXTest
{
    // Тестовый классpublicclass XTest
    {
        // Функция, сообщающая пользователю о своём вызове при помощи// диалогового окнаpublicstaticvoid Test()
        {
            MessageBox.Show("Hello World from test strong named assembly");
        }
    };
}

Скомпилировать данный файл следует при помощи следующей команды

csc /target:library Some.cs

Где параметр /target:library указывает на необходимость создания библиотеки. То есть классы данной сборки можно будет использовать из других приложений. В результате компиляции мы должны получить файл Strong.dll. Это и есть строго именованная однофайловая сборка.

В использовании атрибута AssemblyKeyFile, есть один тонкий момент, - данный атрибут обязательно должен располагаться в глобальном пространстве имён сборки. Для того, чтобы убедиться в этом будем действовать от противного. Поместим его в пространство имён nmXTest, после чего попробуем откомпилировать сборку. Компилятор, явно обидится на нас, о чем не приминет сообщить.

Strong.cs(28,1): error CS0657: 'assembly' is not a valid attribute location for this declaration. Valid attribute locations for
        this declaration are 'type'

Более, особых требований на использование данного атрибута не накладывается.

Теперь, когда мы создали строго именованную сборку, было бы логично научиться её использовать. Но не всё так просто. Использовать данную сборку в персональном режиме не интересно, поскольку то же самое мы делали ранее. А в совместном режиме мы её использовать пока не можем, так как не знаем, как пользоваться глобальным хранилищем сборок (GAC). Посему, придётся несколько отвлечься от линии нашего повествования и рассказать о GAC.

Глобальное хранилище сборок (GAC)

Аббревиатура GAC расшифровывается как Global Assembly Cache, что в дословном переводе на русский означает: Глобальный Кеш Сборок. Но я буду использовать термин хранилище вместо кеш. Он более точно отвечает реальному предназначению GAC, так как кеш это все-таки не постоянное, а временное место хранения чего-либо, используемое для оптимизации доступа к данным. Для примера, можем привести, дисковый или процессорный кеш.

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

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

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

%WINDIR%\assembly\.

Если вы попытаетесь просмотреть содержимое данного каталога при помощи проводника (Explorer’a), то увидите список всех проинсталлированных строго именованных сборок. Подобный тому, что представлен на рисунке 10.


Рисунок 10

Но знайте, данный список не соответствует реальному строению GAC. Он формируется при помощи расширения оболочки проводника Windows.

Расширение оболочки для управления сборками в GAC

Расширение оболочки GAC предназначено для облегчения процесса управления сборками простыми пользователями, а так же администраторами среды исполнения .NET. Которые, вовсе не обязаны знать реальную архитектуру GAC. При открытии папки assembly, расширение оболочки автоматически, собирает информацию обо всех установленных сборках и выводит их в удобном для просмотра виде.

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

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


Рисунок 11


Рисунок 12

На вкладке General, вы можете обнаружить поле CodeBase, значение которого с первого взгляда может несколько смутить. Поскольку в нём указывается путь до сборки, который может вести явно не GAC. На самом деле, данное поле указывает не текущее местоположение сборки, а то откуда производилась её установка в GAC. В случае с обычными сборками данное поле не представляет интереса. А вот если сборка перед использованием скачивалась из сети, мы сможет узнать точный URL адрес её базового местоположения.

Слева в дереве каталогов, под папкой assembly есть папка Download, в ней отображается содержимое кеша закаченных из сети сборок. Фактически данная папка ничего общего с GAC не имеет, поскольку храниться отдельно от него, в учетных записях персонально для каждого пользователя. При изучении данной папки, как раз и пригодиться поле CodeBase, закладки General. Скорее всего, на вашем компьютере папка Download будет пустовать. Удивляться этому не стоит, так как пока использование возможности хранения сборок в сети используется мало. Происходит это по двум причинам: во-первых, о данной возможности мало кто знает, во-вторых, распределённые сетевые приложения пока не пользуются большой популярностью.

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

Расширение оболочки добавляет на панель инструментов малоприметную кнопу (рисунок 13), которая связана с пунктом меню Tools->Cashe Settings.


Рисунок 13

Выбрав пункт меню или нажав на кнопку панели инструментов, вы откроете окно, при помощи которого можно просмотреть общую статистику о глобальном хранилище сборок (рисунок 14).


Рисунок 14

В поле Cache space, отражен общий размер скачанных из сети сборок. А в нижней части окна, при помощи переключателя Download, вы можете установить максимальный размер хранилища скачиваемых сборок.

Расширение оболочки представляет собой COM компонент, который располагается в файле Shfusion.dll.

Реальное строение GAC

Изучить реальную структуру GAC можно несколькими способами:

Расширение оболочки подключается к проводнику при помощи фала Desktop.ini, располагающегося в каталоге assembly. Найдя данный файл в папке assembly, проводник автоматически вызывает расширение оболочки. Следовательно, убрав или переименовав данный файл, мы отключим расширение. Для этого следует выполнить следующие три команды.

        // Переходим в каталог GAC
cd %windir%\assembly
// Снимаем атрибуты системный и скрытый с файла Desktop.ini
attrib –s –h desktop.ini
// Переименовываем файл, дабы отключить расширение оболочки
ren Desktop.ini Desktop.ini.disabled

После отключения расширения, структура каталогов GAC будет отображаться проводником в истинном виде (рисунок 15).


Рисунок 15

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

        // Снимаем с файла атрибуты
attrib -s -h -r desktop.ini
// Удаляем созданный Windows файл
del desktop.ini
// Восстанавливаем исходный
ren desktop.ini.disabled desktop.ini

В каталоге assembly, содержаться четыре вложенных папки:

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

C:\dir %windir%\assembly\GAC
 Volume in drive C is HELLO
 Volume Serial Number is A0FA-F69F

 Directory of C:\WINDOWS\assembly\GAC

<DIR>          .
<DIR>          ..
<DIR>          Microsoft.Vsa.Vb.CodeDOMProcessor
<DIR>          Microsoft_VsaVb
<DIR>          Microsoft.Vsa
<DIR>          Microsoft.VisualBasic.Vsa
<DIR>          cscompmgd
<DIR>          Microsoft.JScript
<DIR>          Microsoft.VisualBasic
<DIR>          Microsoft.VisualC
<DIR>          Regcode
<DIR>          System.EnterpriseServices
<DIR>          System.Security
<DIR>          CustomMarshalers
<DIR>          Accessibility
<DIR>          System.Configuration.Install
<DIR>          System.DirectoryServices
<DIR>          System.Drawing.Design
<DIR>          System.ServiceProcess
<DIR>          System.Web
<DIR>          System.Web.RegularExpressions
<DIR>          System.Web.Services
<DIR>          System.Windows.Forms
<DIR>          System.Xml
<DIR>          System.Data
<DIR>          System.Design
<DIR>          System.Drawing
<DIR>          System
<DIR>          System.Messaging
<DIR>          IEExecRemote
<DIR>          IEHost
<DIR>          IIEHost
<DIR>          ISymWrapper
<DIR>          mscorcfg
<DIR>          System.Data.OracleClient
<DIR>          System.Management
<DIR>          System.Runtime.Remoting
<DIR>          rialization.Formatters.Soap
<DIR>          System.Web.Mobile
<DIR>          Microsoft.Interop.Security.AzRoles
<DIR>          vjswfchtml
<DIR>          vjswfccw
<DIR>          VJSWfcBrowserStubLib
<DIR>          vjswfc
<DIR>          vjslibcw
<DIR>          vjslib
<DIR>          VJSharpCodeProvider
<DIR>          vjscor
<DIR>          SoapSudsCode
<DIR>          System.CF.Windows.Forms.DataGrid
<DIR>          System.CF.Package
<DIR>          Microsoft.CF.WindowsCE.Forms
<DIR>          System.CF.Design
<DIR>          System.CF.Drawing
<DIR>          System.CF.Windows.Forms
<DIR>          ConMan
<DIR>          ConManDataStore
<DIR>          ConManServer
<DIR>          emucm
<DIR>          extractsdk
<DIR>          msddsp
<DIR>          msddslmp
<DIR>          Extensibility
<DIR>          EnvDTE
<DIR>          Office
<DIR>          Microsoft.StdFormat
<DIR>          Microsoft.mshtml
<DIR>          MSDATASRC
<DIR>          ADODB
<DIR>          TlbExpCode
<DIR>          TlbImpCode
<DIR>          Microsoft.VisualBasic.Compatibility.Data
<DIR>          Microsoft.VisualBasic.Compatibility
<DIR>          Microsoft.VisualStudio.VCCodeModel
<DIR>          MCppCodeDomProvider
<DIR>          Microsoft.VisualStudio.VCProjectEngine
<DIR>          Microsoft.VisualStudio.VCProject
<DIR>          stdole
<DIR>          Microsoft.VisualStudio.VSHelp
<DIR>          VSLangProj
<DIR>          Strong
               0 File(s)              0 bytes
              81 Dir(s)     971 382 784 bytes free

При первом взгляде может возникнуть вопрос, а что происходит со сборками, имеющими одинаковые символьные имена. Не произойдёт ли конфликта, при их размещении в этих папках. Для того, чтобы ответить на данный вопрос, необходимо спуститься на один уровень ниже и заглянуть в одну из папок, перечисленных выше. Для этих целей выберем папки System и Strong (она создана мною в тестовых целях), список их содержимого приведён ниже.

C:\>dir %windir%\assembly\gac\system
 Volume in drive C is HELLO
 Volume Serial Number is A0FA-F69F

 Directory of C:\WINDOWS\assembly\gac\system

06.07.2003  07:43    <DIR>          .
06.07.2003  07:43    <DIR>          ..
06.07.2003  07:43    <DIR>          1.0.5000.0__b77a5c561934e089
               0 File(s)              0 bytes
               3 Dir(s)     971 296 768 bytes free

C:\>dir %windir%\assembly\gac\strong
 Volume in drive C is HELLO
 Volume Serial Number is A0FA-F69F

 Directory of C:\WINDOWS\assembly\gac\strong

27.07.2003  12:29    <DIR>          .
27.07.2003  12:29    <DIR>          ..
27.07.2003  12:29    <DIR>          1.0.0.0_ru-RU_51f6c89de8953887
               0 File(s)              0 bytes
               3 Dir(s)     971 296 768 bytes free

На данном уровне происходит более детализированное разделение хранимых сборок. Теперь каталоги уже содержат информацию о версии, региональной принадлежности, а так же маркер открытого ключа. Имена каталогов на данном уровне формируются по следующему правилу: ВерсияСборки_РегиональныйИдентификатор_МаркерОткрытогоКлюча. Вся эта информация вкупе гарантирует уникальность конечного пути хранения сборки и исключает возможные конфликты. Внутри данных папок уже непосредственно содержаться искомые сборки.

C:\>dir %windir%\assembly\gac\strong\1.0.0.0_ru-RU_51f6c89de8953887
 Volume in drive C is HELLO
 Volume Serial Number is A0FA-F69F

 Directory of C:\WINDOWS\assembly\gac\strong\1.0.0.0_ru-RU_51f6c89de8953887

27.07.2003  12:29    <DIR>          .
27.07.2003  12:29    <DIR>          ..
27.07.2003  12:29             3 584 Strong.dll
27.07.2003  13:06               212 __AssemblyInfo__.ini
               2 File(s)          3 796 bytes
               2 Dir(s)     970 403 840 bytes free

Помимо самой сборки, в данном каталоге располагается файл __AssemblyInfo__.ini. Он содержит общую информацию о строгом имени сборки, а так же о начальном местоположении (CodeBase). Данный файл используется средой исполнения в целях оптимизации загрузки, а так же расширением оболочки для хранения информации CodeBase.

Из всего вышесказанного, можно заключить, что GAC является самоописываемой файловой структурой. Вся необходимая информация заключена, в именах папок. Дополнительных источников хранения информации, вроде реестра или каталогов Active Directory, не используется. При всей мощи хранилища GAC, его элегантная простота является революционной, ничего подобного ранее не существовало!

Инсталляция сборок в GAC

Всего существует четыре способа инсталляции сборок в GAC:

Но все они в итоге сводятся к банальному созданию необходимых в %windir%\assembly\GAC каталогов и копированию в них файлов.

Наиболее простой способ инсталляции сборок – воспользоваться расширением оболочки проводника. Для инсталляции необходимо перетащить строго именованную сборку в каталог %windir%\assembly. Для удаления её из GAC необходимо щелкнуть правой кнопкой мыши на необходимой сборке и выбрать из контекстного меню пункт Delete. Данный способ работы со сборками предназначен для пользователей и администраторов, - это очевидно из простоты его использования.

Программистам Microsoft предлагает использовать утилиту gacutil.exe. Для работы с ней вам необходимо уметь пользоваться двумя основными ключами.

/I <ИмяФайлаСборки> - Инсталлирует сборку в GAC
/U <СтрогоеИмяСборки> - Удаляет сборку с заданным строгим именем из GAC
/U <СимвольноеИмяСборки> - Удаляет все сборки с данным символьным именем из GAC

Для того, чтобы протестировать работу данной утилиты, инсталлируем в GAC сборку Strong.dll, созданную в предыдущем разделе. Для этого воспользуемся следующей командой.

gacutil /i Strong.dll

Если все было сделано правильно, утилита должна сообщить об успехе инсталляции при помощи следующего сообщения

Assembly successfully added to the cache

Отдельно хотелось бы прокомментировать работу ключа /U. Если в качестве параметра данного ключа указать только символьное имя сборки, то все сборки с таким символьным именем, не зависимо от других параметров строгого имени, будут удалены. Фактически будет удалён каталог %windir%\assembly\GAC\заданное_символьное_имя_сборки и все его содержимое. Такого режима использования ключа стоит избегать. Так как в данном каталоге могут оказаться сборки других производителей. Ведь совпадения на уровне символьных имён сборок вполне допускаются. Более предпочтительно использовать полное строгое имя сборки, такого вида

Strong,Version=1.0.0.0,Culture=ru-RU,PublicKeyToken=51f6c89de8953887

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

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

Итак, для создания папки Strong, необходимо выполнить следующую команду.

mkdir %windir%\assembly\GAC\Strong

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

ПРИМЕЧАНИЕ

Маркер открытого ключа произвольной сборки можно узнать при помощи утилиты sn.exe, запустив её со следующими параметрами sn.exe –t <ИмяФайлаСборки>.

Каталогу необходимо дать имя по следующему правилу: ВерсияСборки_РегиональныйИдентификатор_МаркерОткрытогоКлюча. В нашем случае это будет выглядеть так.

mkdir %windir%\assembly\GAC\Strong\1.0.0.0_ru-RU_51f6c89de8953887\

Теперь необходимо скопировать в данный каталог нашу сборку.

copy Strong.dll %windir%\assembly\gac\Strong\1.0.0.0_ru-RU_51f6c89de8953887

Помимо самой сборки, во всех каталогах созданных стандартными средствами содержится файл __AssemblyInfo__.ini. Он используется только для оптимизации поиска информации. Если среда исполнения его не найдёт, то информация будет загружена напрямую из сборки. Мы данный файл создавать не будет, все будет прекрасно работать и без него.

Деинсталляция же сборки вручную производится простым удалением соответствующих каталогов и их содержимого.

Из всего сказанного в данном разделе плавно вытекает, то факт что инсталляция приложений .NET сводиться лишь простому копированию файлов. Что делает данную систему крайне устойчивой и надёжной.

Проверка целостности сборки при инсталляции в GAC

Любая сборка, инсталлируемая в GAC, обязана иметь цифровую RSA подпись, удостоверяющая её целостность. Если после последней компиляции сборки её содержимое случайно или преднамеренно изменилось, то проверка подписи сборки перед её инсталляцией не пройдёт. Это легко показать на примере. Возьмём уже созданную и соответственно подписанную сборку Strong.dll. Дизассемблируем её при помощи утилиты ildasm.exe, используя следующую команду.

Ildasm.exe Strong.dll /out:Strong.il

Изменим её содержимое. Для простоты поменяем строку выводимую данной сборок на консоль.

Было
IL_0000:  ldstr      "Hello World from test strong named assembly"
Стало
IL_0000:  ldstr      "He-he I try to hack you"

После чего, скомпилируем сборку, при помощи данной команды, предварительно удалив настоящую сборку Strong.dll

ilasm.exe /dll Strong.il

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

Теперь попытаемся проинсталлировать данную сборку в GAC.

gacutil.exe /i Strong.dll 

Но у нас, к счастью, ничего не выйдет. Поскольку не пройдёт проверка цифровой подписи. О чем с радостью сообщит утилита gacutil, при помощи следующих строк.

Failure adding assembly to the cache: Strong name signature could not be verified.  Was the assembly built delay-signed?

Использование строго именованных сборок

Данный раздел будет завершающим в теме про строго именованные сборки. Мы научимся делать то, ради чего создавались GAC и технология строго именованных сборок, - использовать сборки со строгим именем в совместном режиме. Для введения в тему будет показан наиболее простой способ использования строго именованных сборок: при помощи среды разработки Visual Studio .NET. После этого, для окончательного закрепления материала то же самое будет проделано при помощи утилит командной строки.

ПРЕДУПРЕЖДЕНИЕ

Здесь в качестве совместной мы будет использовать сборку Strong, созданную в одном из предыдущих разделов. Прежде чем создавать приложение, использующее совместную сборку, убедитесь в том, что она проинсталлирована в GAC. Лучше всего сделать это заранее ещё до начала создания проекта.

Во-первых, нам понадобиться приложение, к которому мы будем подключать строго именованную сборку. Создадим простое оконное приложение. Сделать это можно при помощи мастера, который доступен из меню: File->New Project.

После создания проекта необходимо добавить ссылку на сборку, которую мы хотим в нём использовать. Для этого необходимо воспользоваться окном Add Reference, которое может быть вызвано из основного меню - Project->Add Reference или из контекстного меню элемента Reference, закладки Solution Explorer (рисунок 16)


Рисунок 16

В окне Add Reference, необходимо выбрать вкладку .NET. (рис 10). На ней, расположен список, в котором перечислено множество сборок. Среди которых, как было бы справедливо предположить, должна находиться наша сборка Strong, проинсталлированная в GAC. Но её там не окажется. Потому что на самом деле в данном списке перечислены сборки не из GAC, а расположенные в специальных служебных каталогах. Если вас заинтересует их местоположение, то обратите внимание на колонку Path, в ней будет указана необходимая информация.


Рисунок 17

Нам же для того, чтобы добавить связь со сборкой придётся воспользоваться кнопкой Browse, расположенной в правой части окна.

ПРИМЕЧАНИЕ

Если вам часто придётся подключать сборки к проекту, то пользоваться кнопкой Browse, крайне неудобно. Вы можете заставить IDE Visual Studio .NET. отображать в списке сборки из указанного вами каталога. Для этого необходимо добавить в ветвь реестра HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\..NETFramework\AssemblyFolders подветвь с любым именем, и со значением по умолчанию, соответствующим необходимому каталогу. Вы можете сделать это, модифицировав приведённый ниже скрипт реестра.

REGEDIT4
;
;   Листинг 8
;   File:   Exe.reg
;   Author: Dubovcev Aleksey
;

; После запуска данного скрипта Visual Studio.NET начнёт отображать
; сборки из каталога c:\MyAssemlby.
[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\.NETFramework\AssemblyFolders\MyAsm]
@="C:\\MyAssembly"

Здесь необходимо пояснить, что представляет собой добавление связи со сборкой в среде Visual Studio.NET. При разработке приложения сборка необходима на двух этапах:

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

После добавления ссылки на сборки, можно будет исследовать её содержимое при помощи Object Browser, который можно вызвать при помощи меню View->Other Windows->Object Browser. В дереве объектов серенькими прямоугольниками обозначены сборки, на которые установлены в проекте, среди них должна присутствовать сборка Strong (рисунок 18).


Рисунок 18

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

nmXTest.XTest.Test();

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

ПРИМЕЧАНИЕ

Полный код приложения вы сможете найти в листинге 9.

При наборе кода, вы сможете насладиться технологией IntelliSence, подсказывающей содержание типов, прямо по ходу редактирования (рисунок 19).


Рисунок 19

На этом создание программы закончено, теперь осталось лишь откомпилировать и запустить её. Благо из среды Visual Studio .NET это делается одним нажатием кнопки мыши.

На экране должно появиться диалоговое окно с надписью: “Hello World from test strong named assembly” (рисунок 20).


Рисунок 20

Код, выводящий данное диалоговое окно содержится в сборке Strong, которая располагается в GAC.

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

Для того, чтобы убедиться в том, что данное диалоговое действительно выводиться сборкой расположенной в GAC, проведём эксперимент. Удалим сборку из GAC, после чего попытаемся запустить приложение.

В результате мы получим сообщение о том, приложение было остановлено при причине произошедшего необработанного исключения (рисунок 21).


Рисунок 21

Описание, произошедшего исключения указывает на то, что не был найдена сборка Strong. Это говорит о том, что сборка действительно подгружается из GAC.

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

Теперь напишем программу, использующую совместную сборку не прибегая к помощи, среды разработки. Что, впрочем, окажется не намного сложнее. Это будет консольное приложение код, которого приведён ниже.

        /*
    Листинг 7
    File:   Mod.cs
    Author: Dubovcev Aleksey
*/
        // Подключаем основное пространство имён общей библиотеки классов
        using System;

// Основной класс приложенияclass App
{
    // Точка входа в приложениеpublicstaticvoid Main()
    {
        // Ради этой строки создавалось данное приложение, здесь// мы вызовем функцию из совместной сборки// В данном случае вызов происходит по полному имени, // включающего пространство имён. В реальных приложениях// чаще всего пространство имён подключается глобально// при помощи директивы using. (В нашем случае using nmXTest;)
        nmXTest.XTest.Test();
    }
};

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

csc /r:Strong.dll Some.cs

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

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

        // Это ссылка на нашу сборку со строгим именем
.assembly extern Strong
{
    // Маркер открытого ключа, сборки гарантирующий уникальность// её строгого имени
  .publickeytoken = (51 F6 C8 9D E8 95 38 87 )           // Q.....8.// Необходимая версия сборки Strong
  .ver 1:0:0:0
}

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

Strong, Version=1.0.0.0, Culture=neutral, PublicKeyToken=51f6c89de8953887

Компилятор командной строки для удобства автоматически подключает некоторые стандартные сборки. Они перечислены в файле csc.rsp, который храниться вместе с компилятором (csc.exe) в основном каталоге среды исполнения (%windir%\Microsoft.NET\xxxx). Содержимое данного файла представлено ниже.

# This file contains command-line options that the C#
# command line compiler (CSC) will process as part
# of every compilation, unless the "/noconfig" option
# is specified. 

# Reference the common Framework libraries
/r:Accessibility.dll
/r:Microsoft.Vsa.dll
/r:System.Configuration.Install.dll
/r:System.Data.dll
/r:System.Design.dll
/r:System.DirectoryServices.dll
/r:System.dll
/r:System.Drawing.Design.dll
/r:System.Drawing.dll
/r:System.EnterpriseServices.dll
/r:System.Management.dll
/r:System.Messaging.dll
/r:System.Runtime.Remoting.dll
/r:System.Runtime.Serialization.Formatters.Soap.dll
/r:System.Security.dll
/r:System.ServiceProcess.dll
/r:System.Web.dll
/r:System.Web.Mobile.dll
/r:System.Web.RegularExpressions.dll
/r:System.Web.Services.dll
/r:System.Windows.Forms.Dll
/r:System.XML.dll

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

сsc @MyRsp.rsp Some.cs

Помимо ссылок на сборки, в rsp файле, могут быть указаны любые параметры командной строки компилятора. К примеру rsp файл может выглядеть так

# Это файл дополнительный параметров для компилятора csc
/t:library
/r:Strong.dll
Some.cs

Данный файл предписывает компилятору создать библиотечную сборку, использующую Strong.dll.

Внутренний формат имён

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

nmXTest.XTest.Test();

транслируется компилятором в следующую инструкцию

call       void [Strong]nmXTest.XTest::Test()

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

call       void [.module Strong]nmXTest.XTest::Test()

Для местных типов, имя сборки не указывается, хотя оно и присутствует в неявном виде в .NET байт коде.

Сказанное выше верно на уровне IL кода, который является удобочитаемой интерпретацией .NET байт кода. На физическом уровне полные имена, конечно же, не задаются при помощи строки, в начале которой указывается имя сборки, обрамлённое квадратными скобками. Имя формируется при помощи нескольких специальных таблиц и ссылок между ними. Но нас вполне устроит уровень IL кода. Здесь имена задаются полностью (директив подключения пространств имён не существует) в следующем формате.

[ИмяСборки]ПространствоИмён::Имя

Политика версий для строго именованных сборок

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

ПРЕДУПРЕЖДЕНИЕ

Политика версий применяется только для строго именованных сборок.

Таким образом, политика версий действует только на сборки, а на другие программные элементы (классы, интерфейсы, и т.д.) влияния не оказывает.

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

Информация о версии

Информация о версии сборок состоит из четырёх чисел. Первые два называются основной частью версии, а вторые дополнительной. Плюс каждое из них имеет собственное имя, перечислим их в соответствии с порядком следования в версии: Major, Minor, Build и Revision (рисунок 22).


Рисунок 22

Где:

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

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

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

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

Работаем с информацией о версии

Для работы с информацией о версии на уровне исходного кода предназначен атрибут

System.Reflection.Assembly.VersionAttribute

Использование данного атрибута отличается от обычных и требует специального синтаксиса. В языке C# это выглядит так.

        using System.Reflection;

[assembly: AssemblyVersion("1.0.0.0")]

или же так


[assembly: System.Reflection.AssemblyVersion("1.0.0.0")]

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

Версию для данного атрибута можно указывать не полностью. Существует возможность

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

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

Версия в исходном коде Реальная версия
1 1.0.0.0
1.1 1.1.0.0
1.1.* 1.1.[количество дней прошедших с января 2000].[количество секунд прошедших с полуночи делённое на два]
1.1.1 1.1.1.0
1.1.1.* 1.1.1. [количество секунд прошедших с полуночи делённое на два]
1.1.1.1 1.1.1.1
Таблица 2

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

Вначале я решил, что информация о версии всё-таки формируется не компилятором, а самим атрибутом AssemblyVersionAttribute. Но, заглянув в код атрибута, я понял, что это не так.

[AttributeUsage (AttributeTargets.Assembly, AllowMultiple=false)]  
publicsealedclass AssemblyVersionAttribute : Attribute 
{
    private String m_version;

   public AssemblyVersionAttribute(String version)
   {
        m_version = version;
    }

    public String Version
   {
        get { return m_version; }
    }
}

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

Дополнительная информация о версии

Помимо сухой числовой информации о версии существует возможность задать дополнительную информацию, в виде понятной каждому строки. Для этого предназначен атрибут System.Reflection.AssemblyInformationalVersion. Его применение на языке C# выглядит так.

        using System.Reflection;

[assembly: AssemblyInformationalVersion("This is super programm")]

или так

[assembly: System.Reflection.AssemblyInformationalVersion("This is super programm")];

Информацию, заданную при помощи данного атрибута можно просмотреть на вкладке Version, в диалоге свойств файла, выбрав пункт Product Version.


Рисунок 23

Данная информация очень полезна, если требуется наглядно показать различие версий приложений. К примеру, таким способом можно указать на различие между Education Edition и Professional Edition.

Технология прямого запуска

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

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

Напомним, что политика управления версиями применяется только к строго именованным сборкам. А строгое имя неотъемлемо включает в себя версию сборки. Следовательно, строгие имена сборок, у которых будут различаться только версии, - для среды исполнения будут полностью различны. Сборки будут автоматически подгружены, и проблем с их использованием не возникнет (рисунок 24).


Рисунок 24

Поясним рисунок. Обратить внимание стоит, прежде всего, на сборку yyy.dll, её две разные версии одновременно используются приложением. Одна запрашивается непосредственно самой программой, а вторая косвенно через сборку x.dll. Обе эти сборки используются одновременно и не знают о существовании друг друга, несмотря на то, что обе загружены в одном адресом пространстве.

Ранее это было невозможно, поскольку загрузчик операционной системы не различает версии динамических библиотек. Соответственно для него одинаковые файловые имена библиотек предполагают одинаковые библиотеки, даже если у них различный код. Что, конечно же, в корне является не верным. Данная проблема является одной из причин «ада динамических библиотек». Как видите, она с успехом решена в .NET. Но не тут, то было. Как говориться: «хвост вытянешь - ноги увязнут, ноги вытянешь – хвост увязнет».

Хотя технология прямого запуска и предполагает изоляцию различных версий сборок друг от друга, тем не менее, здесь кроется одна очень неприятная и страшная проблема. При использовании (даже неявно) двух сборок различных версий, весьма вероятны конфликты при использовании объектов операционной системы, для которых возможен совместный доступ. Простейшим примером таких объектов являются обычные файлы. Доступ к ним производится при помощи их символьных имён, соответственно если две версии одной и той же библиотеки попытаются одновременно использовать один и тот же файл, то коллизии, скорее всего, избежать не удастся. Либо не будут работать обе библиотеки, или же одна из них, здесь все будет зависеть от способа доступа к файлу. Файлы, конечно же не являются единственными проблемными объектами, на самом деле их множество. Перечислим некоторый из них: файлы, именованный файлы проецируемые в память, сокеты, порты, события, семафоры, ветки реестра, а так же огромное множество объектов, которые перечислять не имеет смысла. Поскольку многие косвенно взаимодействуют с другими объектами, которые в свою очередь могут оказаться проблемными. Для того, чтобы точно знать все подводные камни, необходимо очень хорошо разбираться в устройстве операционной системы, а так же во внутренней реализации FCL. Разработчики FCL, кончено же старались огородить пользователей от этих проблем, автоматизировав по возможности работу со всеми именованными и совместными объектами. Но полностью этого сделать принципиально не возможно. Посему при создании совместных сборок, которые будут, использоваться несколькими приложениями, необходимо быть предельно осторожным.

ПРЕДУПРЕЖДЕНИЕ

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

В качестве совета можно порекомендовать сделать имена всех совместных объектов уникальными. Осуществить это можно несколькими способами, либо динамически генерировать имена, по ходу исполнения программы, используя динамическую генерацию GUID (CoCreateGuid), либо включать в имена версию сборки или даже её полное строгое имя, обязательно включая маркер открытого ключа.

Процесс загрузки .NET приложений

Любая программа .NET будь то exe файл или сборка, прежде всего, является файлом формата PE, запускаемым стандартными средствами. Следовательно, она должна, так или иначе, взаимодействовать с загрузчиком исполняемых файлов Windows. Признаться сперва я решил, что Microsoft пошла наиболее лёгким путём и модифицировала стандартный загрузчик файлов операционных систем. Но, ознакомившись с механизмом загрузки приложений .NET, меня поразила элегантность решения найденного программистами Microsoft. Они сделали чудо – заставили загружаться приложения .NET подобно родным, не изменяя при этом код операционной системы. Здесь, конечно же, необходимо оговориться, - загрузчики исполняемых файлов новых версий операционных систем, конечно же, будут знать об исполняемых файлах .NET, но старые модифицированы не будут.

Все программы .NET представляют собой байт код, для исполнения которого необходима виртуальная машина .NET. Загрузчик исполняемых файлов операционной системы, ничего естественно не знает о байт коде и передаёт код файла на исполнение процессору. Как же тогда вступает в игру виртуальная машина .NET? Оказывается, все просто, - как впрочем, и все гениальное.

Каждый файл приложения .NET имеет в таблице импорта ссылку на библиотеку mscoree.dll, которая является загрузчиком виртуальной машины. Mscoree расшифровывается как Microsoft Component Object Runtime Execution Engine.

ПРИМЕЧАНИЕ

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

После проецирования исполняемого файла .NET, загрузчик Windows сканирует таблицу импорта и загружает все указанные там библиотеки. В том числе и mscoree.dll (в большинстве приложений .NET она будет единственной в таблице импорта). Знающие люди наверняка предположили, что на этом этапе происходит загрузка самой виртуальной машины .NET, из функции DllMain библиотеки mscoree.dll. Ан нет, эта библиотека не имеет данной функции. Все несколько хитрее.

После загрузки всех необходимых библиотек, а так же подготовки адресного пространства приложения, загрузчик Windows передает исполнение на точку входа приложения. В случае приложения .NET, мы обнаружим на месте точки входа следующую заглушку.

start   procnear
  jmp ds:_CorDllMain
start   endp

Код заглушки представлен на языке Ассемблера. Он состоит из одного безусловного перехода на функцию _CorDllMain, которая как вы уже наверняка догадались расположена в библиотеке mscoree.dll. Данная функция производит запуск виртуальной машины .NET, которая после старта начинает перемалывать данный исполняемый файл. Она находит .NET байт код расположенный в нем и при помощи метаданных отыскивает в нем точку входа. Затем начинает исполнение файла, передав его код в распоряжение JIT-компилятора.

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

start   procnear
    jmp ds:_CorDllMain
start   endp

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

ПРИМЕЧАНИЕ

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

Далее мы подробно поговорим о случае, когда приложения .NET располагается в динамической библиотеке.

Динамические библиотеки на основе сборок

Судя по названию данного раздела, может показаться, что мы будем говорить об обычных библиотечных сборках. На самом деле все намного интереснее. Здесь речь пойдёт о создании стандартных динамических библиотек при помощи технологий .NET, которые можно будет использовать при помощи базовых средств операционной системы. Продемонстрируем возможности данной технологии на примере. Для этого создадим динамическую библиотеку, экспортирующую функции, код которых будет непременно исполняться под управлением среды .NET. Затем создадим приложение, которое будет использовать данную библиотеку при помощи базового Windows API.

Код будущей библиотеки представлен ниже.

        /*
    Листинг 12
    File:   Mod.cpp
    Author: Dubovcev Aleksey
*/
        // Подключаем стандартную библиотеку
#using <mscorlib.dll>

// Подключаем основное пространство имён стандартной библиотекиusingnamespace System;

// Данная функция будет экспортироваться из нашей библиотеки// стандартным образом, через таблицу экспортируемых функций в // заголовке pe файла библиотекиvoid Test()
{
    // Проинформируем пользователя о том что функция была вызвана
    Console::WriteLine(S"Hello World from .NET DLL");
}

Код библиотеки содержит единственную функцию, доступ к которой будет возможен извне.

ПРИМЕЧАНИЕ

Обратите на модификатор S перед строкой, он указывает на то, что строка должна быть определена в стиле .NET и расположена совместно с IL кодом в метаданных. Иначе она будет расположена, в секции .data PE файла, как в обычном неуправляемом приложении. В этом случае для её использования потребуется использовать специальные механизмы, что может несколько замедлить работу программы.

Для компиляции данного исходного кода в конечную библиотеку нам понадобиться дополнительный файл с определениями экспортируемых функций. Это Some.def и он представлен ниже.

;
;   Листинг 13
;   File:   Some.def
;   Author: Dubovcev Aleksey
;

; Укажем имя файла библиотеки
LIBRARY Some.dll
; В данном разделе указываются экспортируемые функции
EXPORTS
    ; Экспортируем из библиотеки функцию Test, которая 
    ; выведет приветствие на консоль
    Test

Для компиляции исходного кода воспользуемся двумя следующими командами, поочередно вызывающими компилятор и линковщик.

cl Some.cpp /c /clr                      
link Some.obj /DLL /def:Some.def /noentry

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

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

        /*
    Листинг 14
    File:   Use.cpp
    Author: Dubovcev Aleksey
*/
        // Подключаем базовое Windows API
        #include <windows.h>

// Точка входа в приложениеvoid main()
{
    // Описатель библиотеки, которую нам предстоит загрузить.// Это будет именно та библиотека, которую мы создали// при помощи компилятора MC++
    HANDLE hLib;

    // Указатель на функцию в загруженной библиотекеvoid (*pT) (void);

    // Адрес функции в библиотеке
    UINT dwAddr;

    // Загружаем библиотеку
    hLib = LoadLibrary("Some.dll");
    // Если не получилось выходимif (!hLib) return;

    // Отыскиваем в библиотеке функцию из библиотеки
   pT = (void (*) ())GetProcAddress((HMODULE)hLib,"Test");


    // Если нужной нам функции данная библиотека не содержит, то// выходимif (!pT) return;

    // Вызываем функцию
   (*pT)();
        
    // А еще её можно вызвать вот так!
    dwAddr = (UINT)pT;
    __asm call dwAddr;
}

Для компиляции необходимо воспользоваться следующей командой.

cl Use.cpp

В результате работы созданной программы на консоли появятся две строки

Hello World from .NET DLL
Hello World from .NET DLL

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

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

.methodpublic static void modopt([mscorlib]System.Runtime.CompilerServices.CallConvCdecl) 
        Test() cil managed
{
  .vtentry 1 : 1
  .export [1] as Test
  // Code size       11 (0xb)
  .maxstack  1
  IL_0000:  ldstr      "Hello World from .NET DLL"
  IL_0005:  call       void [mscorlib]System.Console::WriteLine(string)
  IL_000a:  ret
} // end of globalmethod Test

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

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

Test procnear
    jmp dword_10003000
Test endp

Интересно, не правда ли? Вызов в никуда, вот как я это обычно называю.

На самом деле, при загрузке данной библиотеки, автоматически инициализируется среда исполнения .NET. (Данный механизм описан в предыдущем разделе). Она динамически перехватывает все связи в нашей библиотеке. И когда мы вызываем функцию Test, то управление неявно передаётся виртуальной машине .NET, которая уже и вызывает настоящую функцию. Таким образом мы можем вызывать управляемый код .NET из обычного.

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

Локализация приложений при помощи сборок

Среда исполнения имеет в своем арсенале мощные, а главное полностью автоматизированные средства локализации приложений. Основным средством локализации являются сателлитные сборки (satellite assembly). Такие сборки содержат регионально адаптированные ресурсы: строки, изображения и другую регионально зависимую информацию. Microsoft настоятельно не рекомендует располагать в сателлитных сборках код, они предназначены лишь для хранения ресурсов.

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

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

ПРИМЕЧАНИЕ

Справедливо может возникнуть вопрос, для чего кроме языка задавать еще и регион? Региональная информация, так построенная из-за существования многочисленных наречий одних и тех же языков. Порой люди, разговаривающие на разных наречиях одного и того же языка, не могут даже понять друг друга. Впрочем, идентификатор региона является опциональным и его можно не задавать.

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

Идентификатор Числовое значение Язык и регион (английский) Язык и регион (русский)
en-US 0x0409 English - United States Английский - США
en-GB 0x0809 English - United Kingdom Английский - Объединенное Королевство (Англия Ирландия Шотландия Графство Уэллс)
ru-RU 0x0419 Russian - Russia Русский - Россия
uk-UA 0x0422 Ukrainian - Ukraine Украинский - Украина
Таблица 3

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

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

      namespace Inside.NET.Chapter4.Listing1
{
    // Подключаем основное пространство имён общей библиотеки классовusing System;
    using System.Globalization;
    using System.Threading;
    using System.Resources;

    // Основной класс нашего приложенияclass Application
    {
        // Менеджер для работы с ресурсами в стиле .NETprivatestatic ResourceManager rm;
        // Точка входа в приложениеpublicstaticvoid Main()
        {
            // Подключаем менеджер ресурсов
            rm = new ResourceManager("MyStrings", typeof(Application).Assembly);
            
            // Выводим на консоль строку, запрошенную по идентификатору// App_Hello_World
            Console.WriteLine(rm.GetString("App_Hello_World"));         

            // ЛОКАЛИЗУЕМ НАШЕ ПРИЛОЖЕНИЕ – Меняем его региональную// принадлежность
            Thread.CurrentThread.CurrentUICulture = new CultureInfo("ru");

            // Выводим на консоль строку, запрошенную из строкового// ресурса по идентификатору App_Hello_World
            Console.WriteLine(rm.GetString("App_Hello_World"));         
        }
    };
}

Теперь нам необходимо создать две версии строковых ресурсов. Первую регионально нейтральную, с которой приложение будет работать по умолчанию, а вторую русскую. Первая будет размещаться в каталоге приложения, в файле MyStrings.txt. Данный файл в нашем случае будет состоять всего из одной строки.

App_Hello_World = Hello World

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

resgen MyStrings.txt MyStrings.resources

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

csc Sample.cs /res:MyStrings.resources

Прежде чем запускать и тестировать приложение необходимо создать для него русские ресурсы. Они обязательно должны располагаться в подкаталоге ru нашего приложения. Это обязательное требование среды исполнения, поскольку именно там она будет искать сателлитную dll, соответствующую русскому языку. Если говорить более обобщенно, то ресурсы должны располагаться в подкаталоге, имя которого совпадает с региональным идентификатором искомого языка.

Итак в каталоге ru, необходимо создать файл MyStrings.ru.txt. Как видите, в его имя так же должен входить региональный идентификатор. К данному файлу помимо этого предъявляется еще одно требование, он обязательно должен быть в формате Unicode.

App_Hello_World = Здравствуй мир!

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

resgen MyStrings.ru.txt MyStrings.ru.resources

А вот из него уже предстоит создать сателлитную сборку. Сделать это можно при помощи утилиты al.

al /out:Sample.Resources.dll /c:ru /embed:MyStrings.ru.resources

Имя сателитной сборки должно формироваться по следующему правилу <Имя Исполняемого Файла Приложения>.Resources.dll.

Теперь наше приложение готово к запуску и тестированию. Результатом его работы должны стать две следующие строки, выведенные на консоль.

Hello World
Здравствуй мир!

Строение локализованного приложения можно схематично изобразить так (рисунок 25).


Рисунок 25

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

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

Using System.Reflection;

[assembly: AssemblyCulture("en-US")]

или так

[assembly: System.Reflection.AssemblyCulture("en-US")]

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

Конфигурирование политики загрузки сборок

Среда исполнения .NET позволяет управлять процессом загрузки сборок, при помощи конфигурационных файлов. Это обычные текстовые файлы в формате XML, которые размещаются в одном с приложением каталоге. Имя конфигурационного файла формируется по следующему правилу: <Имя Исполняемого Файла приложения>.config

То есть, если приложение располагается в файле Some.exe, то его конфигурационный файл должен называться Some.exe.config. Для наглядности приведём пример простого конфигурационного файла.

<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <dependentAssembly>
            <assemblyIdentity name="Strong"
                              publicKeyToken="51F6C89DE8953887"/>
            <codeBase version="1.0.0.0"
                      href="http://lexa:81/one/Strong.dll"/>

         </dependentAssembly>
      </assemblyBinding>
   </runtime>
</configuration>

Любой конфигурационный файл обязан содержать тег <configuration>. Теги, вложенные в данный, разбивают конфигурационный файл на подразделы, каждый из которых отвечает за свою область настроек. Всего может быть восемь следующих подразделов

В данном разделе нас заинтересует только тег <runtime>, поскольку он напрямую связан с политикой управления загрузкой сборкой. Ниже показана структура данного тега.

<configuration>
     <runtime>
          <developmentMode>
          <assemblyBinding>
               <probing>
               <publisherPolicy>
               <qualifyAssembly>
               <dependentAssembly>
                    <assemblyIdentity>
                    <bindingRedirect>
                    <codeBase>
                    <publisherPolicy>

Элементы, находящиеся на данной схеме левее других, должны быть вложены в своих правых соседей. К примеру, тег <runtime> должен быть вложен в тег <configuration> и т.д.

Далее мы подробно рассмотрим каждый из перечисленных тегов.

<developmentMode>

Данный тег имеет следующий прототип

<developmentMode developerInstallation=”true | false”/>

Тег предназначен для разработчиков. В случае если его параметр developerInstallation установлен в true, то среда исполнения помимо обычных каталогов попытается искать сборки в соответствии с путями, указанными переменной среды окружения DEVPATH.

<assemblyBinding>

Сам данный тег является пустым, и служит лишь для объединения вложенных подтегов.

Рассмотрим по порядку все его подтеги.

<probing>

Прототип тега:

<probing privatePath="paths"/>

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

ПРИМЕЧАНИЕ

Сборками, используемыми в персональном режиме, называются те, которые располагаются в одном с приложением каталоге или в одном из подкаталогов.

Для примера, возьмем одно из созданных нами ранее приложений, которое использует сборку. Пусть это будет сборка Strong.dll, которая располагается в одном с приложением каталоге. А теперь переместим данную сборку, в предварительно созданный, подкаталог asm (Имя подкаталога, безусловно, выбрано абсолютно случайно). И попробуем запустить приложение. Естественно попытка запуска с треском провалится и мы увидим на экране диалоговое окно сообщающее о произошедшем во время запуска исключении.


Рисунок 26

Обратите внимание на тип произошедшего исключения, он указан в верхней части окна (System.IO.FileNotFoundException). Для того, чтобы полностью разобраться в чем дело, посмотрим детальную информацию об исключении.

ПРИМЕЧАНИЕ

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


Рисунок 27

ПРИМЕЧАНИЕ

Далее можно смело нажимать на кнопку Break, которая приостановить исполнение приложения.

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

Unhandled Exception: System.IO.FileNotFoundException: File or assembly name Strong, or one of its depe
ndencies, was not found.
File name: "Strong"
   at App.Main()

Fusion log follows:
=== Pre-bind state information ===
LOG: DisplayName = Strong, Version=1.0.0.0, Culture=neutral, PublicKeyToken=51f6c89de8953887
 (Fully-specified)
LOG: Appbase = E:\Projects\.NET\Assembly\Use\Probing\
LOG: Initial PrivatePath = NULL
Calling assembly : Some, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null.
===

LOG: Application configuration file does not exist.
LOG: Publisher policy file is not found.
LOG: Host configuration file not found.
LOG: Using machine configuration file from C:\WINXP\Microsoft.NET\Framework\v1.0.3705\config\machine.c
onfig.
LOG: Post-policy reference: Strong, Version=1.0.0.0, Culture=neutral, PublicKeyToken=51f6c89de8953887
LOG: Attempting download of new URL file:///E:/Projects/.NET/Assembly/Use/Probing/Strong.DLL.
LOG: Attempting download of new URL file:///E:/Projects/.NET/Assembly/Use/Probing/Strong/Strong.DLL.
LOG: Attempting download of new URL file:///E:/Projects/.NET/Assembly/Use/Probing/Strong.EXE.
LOG: Attempting download of new URL file:///E:/Projects/.NET/Assembly/Use/Probing/Strong/Strong.EXE.

Если обратиться к последним строкам данного лога то можно заметить, что среда исполнения пыталась загрузить сборку из дополнительных подкаталогов приложения, а так же из файлов с другими именами. Данный процесс называется зондированием: от английского probing. (He путать с зомбированием!).

Во время зондирования среда исполнения учитывает значение параметра privatePath, тега probing. Следовательно, нам необходимо лишь указать каталог asm, в данном параметре. Конфигурационный файл будет выглядеть так:

<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <probing privatePath="asm"/>
      </assemblyBinding>
   </runtime>
</configuration>

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

<publisherPolicy>

Прототип тега:

<publisherPolicy apply="yes|no"/>

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

<qualifyAssembly>

Прототип тега:

<qualifyAssembly partialName="PartialAssemblyName" fullName="FullAssemblyName"/>

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

К примеру, сделаем ассоциацию для сборки Strong.

<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <qualifyAssembly partialName="Strong" 
                         fullName=
"Strong,version=1.0.0.0,culture=neutral,publicKeyToken=51f6c89de8953887"/>
      </assemblyBinding>
   </runtime>
</configuration>

Теперь везде в нашем приложении можно использовать сокращенное имя Strong, вместо полного, строгого. После введения связи, вызов

Assembly.Load(“Strong”);

будет рассмотрен средой исполнения как.

Assembly.Load(“Strong,version=1.0.0.0,culture=neutral,publicKeyToken=51f6c89de8953887");

<dependentAssembly>

Данный тег позволяет управлять политикой загрузки персонально для каждой сборки. Сам он является пустым и служит лишь для объединения подтегов: <assemblyIdentity>, <BindingRedirect> и <CodeBase>. Первый из которых является обязательным. Он указывает строгое имя сборки, к которой будут применяться теги <BindingRedirect> и <CodeBase>.

<assemblyIdentity>

Прототип тега

<assemblyIdentity 
   name="assembly name"
   publicKeyToken="public key token"
   culture="assembly culture"/>

Параметры имеют следующий смысл:

Для каждой сборки, для которой необходимо задать дополнительные параметры загрузки, необходимо создать тег <dependentAssembly>, в который вложить тег <assemblyIdentity> идентифицирующий сборку. Пример использования данного тега смотри ниже.

<BindingRedirect>

Прототип тега:

<bindingRedirect 
   oldVersion="old assembly version"
   newVersion="new assembly version"/>

Данный тег позволяет принудительно изменить политику управления версиями необходимой сборки. Вы указываете старую версию сборки и новую, которая будет подгружаться вместо неё. К примеру, так:

<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <dependentAssembly>
            <assemblyIdentity name="Strong"
                              publicKeyToken="51f6c89de8953887"
                              culture="neutral" />
            <bindingRedirect oldVersion="1.0.0.0"
                             newVersion="2.0.0.0"/>
         </dependentAssembly>
      </assemblyBinding>
   </runtime>
</configuration>

Теперь если приложение запросит сборку Strong версии 1.0.0.0, то ничего не зная об этом, получит сбоку версии 2.0.0.0.

<codeBase>

Этот тег является наиболее интересным из описываемых здесь. Его прототип:

<codeBase 
   version="Assembly version"
   href="URL of assembly"/>

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

Причем в качестве значения параметра href, может быть указан сетевой адрес. Продемонстрируем это на красочном примере.

<configuration>
   <runtime>
      <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
         <dependentAssembly>
            <assemblyIdentity name="Strong"
                              publicKeyToken="51F6C89DE8953887"/>
            <codeBase version="1.0.0.0"
                      href="http://lexa:81/one/Strong.dll"/>

         </dependentAssembly>
      </assemblyBinding>
   </runtime>
</configuration>

Данный конфигурационный файл предписывает среде исполнения загружать сборку Strong, с адреса http://lexa:81/one/Strong.dll, по протоколу http, через 81й порт. Где lexa, это имя моей рабочей станции.

Для того, чтобы протестировать работу данного конфигурационного файла необходимо установить Web сервер для локального тестирования или выложить сборку на один из серверов в Интернет. У меня на рабочей станции стоят два web-сервера IIS и Apache. А для того, чтобы избежать конфликтов, они разнесены по разным портам 81 и 82 соответственно. Именно по этому я указывал 81 порт в адресе ссылки на сборку. Ниже приведены краткие рекомендации по установке и настройке данных серверов.

Настройка IIS сервера

IIS сервер входит в стандартную поставку операционных систем класса Windows NT, начиная с 2000 версии. Правда он может не входить в начальный пакет установки. Для того, чтобы его инсталлировать, вам придется воспользоваться мастером добавления дополнительных компонент Windows из панели управления (Add or Remove Programs->Add/Remove Windows Components). После установки сервера в корне диска C: появиться папка IntePub. В ней располагается каталог wwwroot – это корневая папка www-сервера, файлы которой доступны по протоколу HTTP. В ней необходимо создать папку one, в которой поместить сборку Strong.dll. После произведённых манипуляций сборка будет доступна по одному из следующих адресов.

http://localhost/one/Strong.dll

http://localhost:80/one/Strong.dll

http://имя_компьютера/one/Strong.dll

http://имя_компьютера:80/one/Strong.dll

Порт 80 можно не указывать, поскольку он по умолчанию используется при работе с протоколом http.

Так же можно воспользоваться специальным апплетом управления Web-сервером ISS (Control Panel->Administrative Tools->Internet Information Services), который позволяет производить более тонкую настройку.

Настройка Apache сервера

Apache сервер настраивается несколько сложнее чем IIS, правда, если вы будете следовать приведённым здесь рекомендациям, то проблем возникнуть не должно.

ПРИМЕЧАНИЕ

Apache сервер совершенно безвозмездно можно загрузить по следующему адресу http://apache.org/. Не забудьте, что вам нужна версия для Windows.

Данный Web сервер не имеет интерактивной среды настройки, все параметры придётся задавать при помощи специального конфигурационного скрипта. Он располагается в файле: …\ conf\httpd.conf, относительно корня сервера. Нам предстоит создать виртуальную связь папки на сервере, с каталогом на диске. Для этого необходимо добавить следующую строку.

Alias /one "C:/Program Files/Apache Group/Apache2/one"

Где первый параметр определяет имя виртуальной директории на сервере, а второй путь до реальной папки на жестком диске.

ПРИМЕЧАНИЕ

Каталог со сборкой может располагаться в любом месте жесткого диска, а не только в одном из подкаталогов Apache сервера.

Естественно в данную папку необходимо поместить сборку Strong.

Проверяем возможность автоматической загрузки сборок из сети

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

ПРИМЕЧАНИЕ

Для ссылок на локальный сервер помимо его реального имени, вроде lexa,можно использовать специально зарезервированные имена. Это localhost, а так же все IP адреса из диапазона 127.0.0.1-127.0.0.255. То есть ссылки могут выглядеть следующим образом.

http://lосalhost/one/Strong.dll

http://127.0.0.1/one/Strong.dll

http://127.0.0.3/one/Strong.dll

http://127.0.0.255/one/Strong.dll

............

Для того, чтобы задать URL для файла располагающегося на жестком диске воспользуйтесь псевдо протоколом file://. К примеру так

file://C:\Strong.dll

При первом запуске приложения сборка будет скачана из сети по указному адресу и добавлена в кеш. Он располагается по следующему адресу на вашем жестком диске

        "\Documents and Settings\Имя Текущей Учёной Записи\Local Settings\Application Data\assembly\dl\".

При повторных запусках сборка будет загружаться уже с жесткого диска компьютера, что уменьшит сетевой трафик. Вы можете просмотреть список скачанных сборок используя расширение оболочки, заглянув в папку %WINDIR%\assembly\dowload.

В заключение данного раздела, необходимо упомянуть, об одном неприятном моменте. При указании сетевого URL избегайте скачивания по протоколу ftp, поскольку в ранних версиях среды исполнения данная возможность попросту не работает. Причем, что странно запросы от среды исполнения, к ftp серверу исправно поступают, в чем можно убедиться исследовал его лог файлы. И даже происходит их скачивание (судя по логам), но подключение данных сборок по неведомым мне причинам не происходит.

Редактирование конфигурационных файлов при помощи встроенных средств среды исполнения

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

Для тех, кого не устраивает редактирование конфигурационных файлов вручную Microsoft создала специальный апплет (Control Panel\Administration Tools\Microsoft .NET Framework Configuration).


Рисунок 28

При помощи них вы можете легко настроить все описанные выше параметры. Правда есть одно но, данный аплет выполнен в виде оснастки консоли управления, которая присутствует только в системах класса NT. А посему, пользователям операционных систем вроде Windows 98 конфигурационные файлы придётся редактировать руками.

Процесс поиска сборок средой исполнения

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

Рассмотрим каждый из данных пунктов более подробно.

Анализ конфигурационного файла

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

Ранее мы рассматривали, как эти параметры могут повлиять на загрузку сборок.

Проверка ранее загруженных сборок

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

Если сборка уже загружена, то среда исполнения просто устанавливает на неё соответствующие внутренние ссылки. Если нет, то среда исполнения приступает к следующему этапу поиска.

Поиск в GAC

Если сборка строго именованная, то среда исполнения сначала пытается отыскать её в GAC. Она обращается к менеджеру глобального хранилища сборок, расположенному в недрах динамической библиотеки fusion.dll. А он уже в свою очередь производит физический поиск сборки. Если это не привело к успеху, то загрузчик среды исполнения пытается отыскать сборку в каталоге приложения. Данный процесс подробно описан в следующем разделе.

Поиск файлов сборок в каталоге приложения

Сначала поиск сборок будет производиться в лоб, в основном каталоге приложения по реальному имени сборки. Если она не будет найдена, то загрузчик среды исполнения запустит процесс зондирования. Дабы не усложнять процесс описания этого механизма, приведём отчет неудачной попытки поиска сборки Strong.dll

Listing.10/Strong.DLL.
Listing.10/Strong/Strong.DLL.
Listing.10/Strong.EXE.
Listing.10/Strong/Strong.EXE.

Достаточно интересным моментом здесь является то, что среда исполнения варьирует расширение файла от DLL до ЕXE.

Просмотр логов загрузки сборок и исключений при помощи утилиты Fuslogvw.exe

При загрузке приложений зачастую возникают проблемы. В случае консольного приложения полный отчет о произошедших исключениях выводиться прямо на консоль. Но что же делать, если приложение не имеет консоли – если оно оконного типа или это вообще Web-приложение. В этом случае для просмотра лога проблем придётся воспользоваться специальной утилитой Fuslogvw.exe, которая входит в поставку .NET Framework SDK от Microsoft. Вид окна данной утилиты приведён на рисунке 29.


Рисунок 29

ПРИМЕЧАНИЕ

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

HKLM\Software\Microsoft\Fusion\ForceLog

равным 1 (данный параметр имеет тип DWORD). Сделать вы это можете либо при помощи всем нам до боли знакомой утилиты regedit или автоматически, прибегнув к помощи следующего reg файла.

REGEDIT4
;
;   Листинг 17
;   File:   Log.reg
;   Author: Dubovcev Aleksey
;


[HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Fusion]
"ForceLog"="1"

После внесения необходимых изменений в реестр, станет возможным просмотр логов при помощи данной утилиты.

Для того, чтобы в списке главного окна утилиты отображались логи сбоев, необходимо установить галочку напротив переключателя Log Failures.

Ниже приведен пример лога сбоя. Для удобства, все комментарии вставлены прямо в его тело, в стиле C подобных языков (// или /**/).

      // В конце данной строки указана дата и время сбоя произошедшего при
      // загрузке приложение
*** Assembly Binder Log Entry  (19.06.2001 @ 1:00:34) ***

The operation failed.
// Указывается код причины сбоя и её описание// Вы можете узнать описание данного кода самостоятельно// используя утилиту ErrorLookup из поставки Visual Studio// или воспользоваться функцией API FormatMessage
Bind result: hr = 0x80070002. The system cannot find the file specified.

Assembly manager loaded from:  
// Это путь к библиотеке, которая производила запись лога// в ней располагается менеджер управляющий сбрками
C:\WINXP\Microsoft.NET\Framework\v1.0.3705\fusion.dll
// А это наше сбойное приложение   
Running under executable  E:\Projects\.NET\Assembly\Use\Some.exe
--- A detailed error log follows. 

// Общая информация известная до загрузки
=== Pre-bind state information ===
// Идентификационные параметры сборки
LOG: DisplayName = Strong, Version=1.0.0.0, Culture=neutral, PublicKeyToken=51f6c89de8953887
 (Fully-specified)
// Папка приложения
LOG: Appbase = E:\Projects\.NET\Assembly\Use\
LOG: Initial PrivatePath = NULL
LOG: Dynamic Base = NULL
LOG: Cache Base = NULL
LOG: AppName = NULL
Calling assembly : Some, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null.

// А вот отсюда начинается поиск сборки// Просмотр путей разработки
LOG: Processing DEVPATH.
LOG: DEVPATH is not set. Falling through to regular bind.
// Попытка загрузить конфигурационный файл приложения
LOG: Attempting application configuration file download.
// Опля файл загружен
LOG: Download of application configuration file was attempted from file:///E:/Projects/.NET/Assembly/Use/Some.exe.config.
LOG: Found application configuration file (E:\Projects\.NET\Assembly\Use\Some.exe.config).
// Никаких исправлений для данной сборки не найдено
LOG: Publisher policy file is not found.
LOG: Host configuration file not found.
LOG: Using machine configuration file from C:\WINXP\Microsoft.NET\Framework\v1.0.3705\config\machine.config.
// Обнаружена политика для загрузки следующей сборки
LOG: Post-policy reference: Strong, Version=1.0.0.0, Culture=neutral, PublicKeyToken=51f6c89de8953887
// Попытка найти сборку в глобальном хранилище провалилась
LOG: Cache Lookup was unsuccessful.
// Пытаемся скачать сборку из сети
LOG: Attempting download of new URL ftp://localhost/osfdne/Strong.dll.// Ни чего у нас не вышло
LOG: All probing URLs attempted and failed.

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

Вот и всё!

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


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