В главе 3 рассказано о ряде более сложных, но очень важных концепций, в том числе способах создания и применения сборок, содержащих компоненты, предназначенные для использования совместно с другими приложениями. В этой и следующей главах также показано, как администратор может влиять на исполнение приложения и его типов.
Современные приложения состоят из типов, которые создаются самими разработчиками или компанией Microsoft. Помимо этого, процветает целая отрасль поставщиков компонентов, которые используются клиентами, чтобы сократить время на разработку проектов. Типы, реализованные при помощи языка, ориентированного на общеязыковую исполняющую среду (CLR), способны легко работать друг с другом, базовый класс такого типа можно даже написать на другом языке программирования.
В этой главе объясняется, как эти типы создаются и упаковываются в файлы, предназначенные для развертывания. В процессе изложения дается крат кий исторический обзор некоторых проблем, решенных с приходом .NET Framework.
Задачи развертывания в .NET Framework
Все годы своего существования операционная система Windows «славилась» нестабильностью и чрезмерной сложностью. Такая репутация, заслуженная или нет, сложилась по ряду причин. Во-первых, все приложения используют динамически подключаемые библиотеки (Dynamic Link Library, DLL), созданные Microsoft и другими производителями. Поскольку приложение исполняет код, написанный разными производителями, ни один разработчик какой-либо части программы не может быть на 100% уверен в том, что точно знает, как другие собираются применять созданный им код. В теории такая ситуация чревата любыми неполадками, но на практике взаимодействие кодов от разных производителей редко становится источником проблем, так как перед развертыванием приложения тестируют и отлаживают.
Однако пользователи часто сталкиваются с проблемами, когда производитель решает обновить поставленную им программу и предоставляет новые файлы. Предполагается, что новые файлы обеспечивают «обратную совместимость» с прежним программным обеспечением, но кто за это поручится? Одному производителю, выпускающему обновление своей программы, фактически не под силу заново протестировать и отладить все существующие приложения, чтобы убедиться, что изменения при обновлении не влекут за собой нежелательных последствий.
Уверен, что каждый читающий эту книгу сталкивался с той или иной разновидностью проблемы, когда после установки нового приложения нарушалась работа одной (или нескольких) из установленных ранее программ. Эта проблема получила название «ад DLL». Подобная уязвимость вселяет ужас в сердца и умы обычных пользователей компьютеров. В конечном итоге пользователи должны как следует обдумать, стоит ли устанавливать новое программное обеспечение на их компьютеры. Что касается меня, то я решил вовсе не пробовать устанавливать некоторые приложения из опасения, что они нанесут вред наиболее важным для меня программам.
Второй фактор, повлиявший на репутацию Windows, - сложности при установке приложений. Большинство приложений при установке умудряются «просочиться» во все части операционной системы. Например, при установке приложения происходит копирование файлов в разные каталоги, модификация параметров реестра, установка ярлыков и ссылок на рабочий стол (Desktop), в меню Пуск (Start) и на панель быстрого запуска. Проблема в том, что приложение - это не одиночная изолированная сущность. Нельзя легко и просто создать резервную копию приложения, поскольку, кроме файлов приложения, придется скопировать соответствующие части реестра. Вдобавок, нельзя просто взять и переместить приложение с одной машины на другую - для этого нужно запустить программу установки еще раз, чтобы корректно скопировать все файлы и параметры реестра. Наконец, приложение не всегда просто удалить - часто остается неприятное ощущение, что какая-то его часть затаилась где-то внутри компьютера.
Третий фактор - безопасность. При установке приложений записывается множество файлов, созданных самыми разными компаниями. Вдобавок, многие веб-приложения (например, ActiveX) зачастую содержат программный код, который сам загружается из Интернета, о чем пользователи даже не подозревают. На современном уровне технологий такой код может выполнять любые действия, включая удаление файлов и рассылку электронной почты. Пользователи справедливо опасаются устанавливать новые приложения из-за угрозы потенциального вреда, который может быть нанесен их компьютерам. Для того чтобы пользователи чувствовали себя спокойнее, в системе должны быть встроенные функции защиты, позволяющие явно разрешать или запрещать доступ к системным ресурсам коду, созданному теми или иными компаниями.
Как показано в этой и следующей главах, платформа .NET Framework устраняет «ад DLL» и делает существенный шаг вперед к решению проблемы, связанной с распределением данных приложения по всей операционной системе. Например, в отличие от модели СОМ компонентам больше не требуется хранить свои параметры в реестре. К сожалению, приложениям пока еще требуются ссылки и ярлыки. Совершенствование системы защиты связано с новой моделью безопасности платформы .NET Framework - безопасностью доступа на уровне кода (code access security). Если безопасность системы Windows основана на идентификации пользователя, то безопасность доступа к коду - на правах, которые контролируются хостом приложений, загружающим компоненты. Такой хает приложения, как Microsoft Silverlight, может предоставить совсем немного полномочий загруженному программному коду, в то время как локально установленное приложение во время своего выполнения может иметь уровень полного доверия (со всеми полномочиями). Как видите, платформа .NET Framework предоставляет пользователям намного больше возможностей по контролю над тем, что устанавливается и выполняется на их машинах, чем когда-либо давала им система Windows.
Компоновка типов в модуль
В этом разделе рассказывается, как превратить файл, содержащий исходный код с разными типами, в файл, пригодный для развертывания. Для начала рас смотрим следующее простое приложение:
{codecitation class="brush: csharp; gutter: true;" width="100%" }public sealed class Program
{
pubic static void Main()
{
System. Consolе. Writeline ("Hi");
}
}
{/codecitation}
Здесь определен тип Program с единственным статическим открытым методом Main. Внутри метода Main находится ссылка на другой тип - System.Console. Этот тип разработан в компании Microsoft, и его программный код на языке IL, реализующий его методы, находится в файле MSCorlib.dll. Таким образом, данное приложение определяет собственный тип, а также использует тип, созданный другой компанией. Для того чтобы скомпоновать это приложение-пример, сохраните этот код в файле Program.cs, а затем наберите в командной строке следующее:
{codecitation class="brush: csharp; gutter: true;" width="100%" }csc.exe /out:Program.exe /t:exe /r:MSCorlib.dll Program.cs {/codecitation}{/codecitation}
Эта команда указывает компилятору С# создать исполняемый файл Program.ехе (имя задано параметром /out: Program.exe). Тип создаваемого файла - консольное приложение Win32 (тип задан параметром /t[arget] :ехе).
При обработке файла с исходным кодом компилятор С# обнаруживает ссылку на метод Writeline типа System.Console. На этом этапе компилятор должен убедиться, что этот тип существует и у него есть метод Writeline. Компилятор также проверяет, чтобы типы аргументов, предоставляемых программой, совпадали с ожидаемыми типами метода Writeline. Поскольку тип не определен в исходном коде на С#, компилятору С# необходимо передать набор сборок, которые позволят ему разрешить все ссылки на внешние типы. В показанной команде параметр /r[eference]: MSCorLib.dll приказывает компилятору вести поиск внешних типов в сборке, идентифицируемой файлом MSCorlib.dll.
MSCorlib.dll - это особый файл в том смысле, что в нем находятся все основные типы: Byte, Char, String, Int32 и т. д. В действительности, эти типы используются так часто, что компилятор С# ссылается на эту сборку (MSCorlib.dll) автоматически. Другими словами, следующая команда (в ней опущен параметр /r) даст тот же результат, что и предыдущая:
{codecitation class="brush: plain; gutter: true;" width="100%" }csc.exe /out: Program.exe /t: exe Program.cs{/codecitation}
Более того, поскольку значения, заданные параметрами командной строки /out: Program.exe и /t:exe, совпадают со значениями по умолчанию, следующая команда даст аналогичный результат:
{codecitation class="brush: plain; gutter: true;" width="100%" }csc.exe Program.cs{/codecitation}
Если по какой-то причине вы не хотите, чтобы компилятор С# ссылался на сборку MSCorlib.dll, используйте параметр /nostdlib. В компании Microsoft задействуют именно этот параметр при компоновке сборки MSCorlib.dll. Например, во время исполнения следующей команды при компиляции файла Program.cs генерируется ошибка, поскольку тип System.Consolе определен в сборке MSCorlib.dll:
{codecitation class="brush: plain; gutter: true;" width="100%" }csc.exe /out: Program.exe /t:exe /nostdlib Program.cs {/codecitation}
А теперь присмотримся поближе к файлу Program.exe, созданному компилятором С#. Что он из себя представляет? Для начала это стандартный файл в формате РЕ (portableexecutable). Это значит, что машина, работающая под управлением 32- или 64-разрядной версии Windows, способна загрузить этот файл и что-нибудь с ним сделать. Система Windows поддерживает два типа приложений: с консольными (Console User Interface, CUI) и графическими пользовательскими интерфейсами (Graphical User Interface, GUI). Параметр /t: ехе указывает компилятору С# создать консольное приложение. Для создания приложения с графическим интерфейсом необходимо указать параметр /t :winexe.
Файл параметров
В завершение рассказа о параметрах компилятора хотелось бы сказать несколько слов о файлах параметров (response files) - текстовых файлах, содержащих набор параметров командной строки для компилятора. При выполнении компилятора CSC.exe открывается файл параметров и используются все указанные в нем параметры, как если бы они были переданы в составе командной строки. Файл параметров передается компилятору путем указания его в командной строке с префиксом @. Например, пусть есть файл параметров MyProject.rsp со следующим текстом:
{codecitation class="brush: plain; gutter: true;" width="100%" }/out:MyProject.exe /target:winexe{/codecitation}
Для того чтобы компилятор (CSC.exe) использовал эти параметры, необходимо вызвать файл следующим образом:
{codecitation class="brush: plain; gutter: true;" width="100%" }csc.exe @MyProject.rsp CodeFile1.cs CodeFile2.cs {/codecitation}
Эта строка сообщает компилятору С# имя выходного файла и тип скомпилированной программы. Очевидно, что файлы параметров исключительно полезны, так как избавляют от необходимости вручную вводить все аргументы командной строки каждый раз при компиляции проекта.
Компилятор С# поддерживает работу с несколькими файлами параметров. Помимо явно указанных в командной строке файлов, компилятор автоматически ищет файл с именем CSC.rsp в текущем каталоге, поэтому относящиеся к проекту параметры нужно указывать именно в этом файле. Компилятор также проверяет каталог с файлом CSC.exe на наличие глобального файла параметров CSC.rsp, в котором следует указывать параметры, относящиеся ко всем проектам. В процессе своей работы компилятор объединяет параметры из всех файлов и использует их. В случае конфликтующих параметров в глобальных и локальных файлах предпочтение отдается последним. Кроме того, любые явно заданные в командной строке параметры имеют более высокий приоритет, чем указанные в локальных файлах параметров.
При установке платформы .NET Framework по умолчанию глобальный файл CSC.rsp устанавливается в каталог %SystemRoot%\Microsoft. NET\Framework\ vX.X.X (где Х.Х.Х- версия устанавливаемой платформы .NET Framework). В версии 4.0 этот файл содержит следующие параметры:
{codecitation class="brush: plain; gutter: true;" width="100%" }Этот файл содержит параметры командной строки,
# которые компилятор С# командной строки (CSC)
# будет обрабатывать в каждом сеансе компиляции,
#если только не задан параметр "/noconfig".
# Ссылки на стандартные библиотеки Framework
/r:Accessibility.dll
/r:Microsoft.CSharp.dll
/r:System.Configuration.dll
/r:System.Configuration.Install.dll
/r:System.Core.dll
/r:System.Data.dll
/r:System.Data.DataSetExtensions.dll
/r:System.Data.Linq.dll
/r:System.Deployment.dll
/r:System.Device.dll
/r:System.DirectoryServices.dll
/r:System.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.dll
/r:System.Runtime.Serialization.Formatters.Soap.dll
/r:System.Security.dll
/r:System.ServiceModel.dll
/r:System.ServiceProcess.dll
/r:System.Transactions.dll
/r:System.Web.Services.dll
/r:System.Windows.Forms.dll
/r:System.Xml.dll
/r:System.Xml.Linq.dll{/codecitation}
В глобальном файле CSC.rsp есть ссылки на все перечисленные сборки, поэтому нет необходимости указывать их явно с помощью параметра /reference. Этот файл параметров исключительно удобен для разработчиков, так как позволяет использовать все типы и пространства имен, определенные в различных опубликованных компанией Microsoft сборках, не указывая их все явно с применением параметра /reference.
Ссылки на все эти сборки могут немного замедлить работу компилятора, но если в исходном коде нет ссылок на типы или члены этих сборок, это никак не сказывается ни на результирующем файле сборки, ни на производительности его выполнения.
Примечание. При использовании параметра /refereпce для ссылки на какую-либо сборку можно указать полный путь к конкретному файлу. Однако если такой путь не указать, компилятор будет искать нужный файл в следующих местах (в указанном порядке):
· Рабочий каталог.
· Каталог, содержащий файл самого компилятора (CSC.exe). Библиотека MSCorlib.dll всегда извлекается из этого каталога. Путь к нему имеет при мерно следующий вид: %SystemRoot%\Microsoft .NET\Framework\v4.0.#####.
· Все каталоги, указанные с использованием параметра /lib компилятора.
· Все каталоги, указанные в переменной окружения LIB.
Конечно, вы вправе добавлять собственные параметры в глобальный файл CSC.rsp, но это сильно усложняет репликацию среды компоновки на разных машинах - приходится помнить про обновление файла CSC.rsp на всех маши нах, используемых для сборки приложений. Можно также дать компилятору команду игнорировать как локальный, так и глобальный файлы CSC.rsp, указав в командной строке параметр /noconfig.
Несколько слов о метаданных
Что же именно находится в файле Program.exe? Управляемый РЕ-файл состоит из 4-х частей: заголовка РЕ32(+), заголовка CLR, метаданных и кода на про межуточном языке (intermediate language, IL). Заголовок РЕ32(+) хранит стандартную информацию, ожидаемую Windows. Заголовок CLR - это небольшой блок информации, специфичной для модулей, требующих CLR (управляемых модулей). В него входит старший и младший номера версии CLR, для которой скомпонован модуль, ряд флагов и маркер MethodDef (о нем - чуть позже), указывающий метод точки входа в модуль, если это исполняемый CUI- или GUI -файл, а также необязательную сигнатуру строгого имени (она рассмотрена в главе 3). Наконец, заголовок содержит размер и смещение некоторых таблиц метаданных, расположенных в модуле. Для того чтобы узнать точный формат заголовка CLR, изучите структуру IMAGE_COR20_HEADER, определенную в файле CorHdr.h.
Метаданные - это блок двоичных данных, состоящий из нескольких та блиц. Существуют три категории таблиц: определений, ссылок и манифестов. В табл. 2.1 приводится описание некоторых наиболее распространенных таблиц определений, существующих в блоке метаданных модуля.
Таблица 2.1. Общие таблицы определений, входящих в метаданные
Имя таблицы определений |
Описание |
ModuleDef |
Всегда содержит одну запись, идентифицирующую модуль. Запись включает имя файла модуля с расширением (без указания пути к файлу) и идентификатор версии модуля (в виде созданного компилятором значения GUID). Это позволяет переименовывать файл, не теряя сведений о его исходном имени. Однако настоятельно рекомендуется не переименовывать файл, иначе среда CLR может не найти сборку во время выполнения |
TypeDef |
Содержит по одной записи для каждого типа, определенного в модуле. Каждая запись включает имя типа, базовый тип, флаги сборки (public, private и т. д.) и указывает на записи таблиц MethodDef, PropertyDef и EventDef, содержащие соответственно сведения о методах, свойствах и событиях этого типа
|
MethodDef |
Содержит по одной записи для каждого метода, определенного в модуле. Каждая строка включает имя метода, флаги (private, public, viгtual, abstract, static, final и т. д.), сигнатуру и смещение в модуле, по которому находится соответствующий IL-код. Каждая запись также может ссылаться на запись в таблице ParamDef, где хранятся дополнительные сведения о параметрах метода |
FieldDef |
Содержит по одной записи для каждого поля, определенного в модуле. Каждая запись состоит из флагов (например, pгivate, public и т. д.) и типа поля |
ParamDef
|
Содержит по одной записи для каждого параметра, определенного в модуле. Каждая запись состоит из флагов (in, out, retval и т. д.), типа и имени |
PropertyDef
|
Содержит по одной записи для каждого свойства, определенного в модуле. Каждая запись включает имя, флаги, тип и вспомогательное поле (оно может быть пустым) |
EventDef
|
Содержит по одной записи для каждого события, определенного в модуле. Каждая запись включает имя и флаги |
Для каждой сущности, определяемой в компилируемом исходном тексте, компилятор генерирует строку в одной из таблиц, перечисленных в табл. 2.1. В ходе компиляции исходного текста компилятор также обнаруживает типы, поля, методы, свойства и события, на которые имеются ссылки в исходном тексте. Все сведения о найденных сущностях регистрируются в нескольких таблицах ссылок, составляющих метаданные. В табл. 2.2 показаны некоторые наиболее распространенные таблицы ссылок, которые входят в состав метаданных.
Таблица 2.2. Общие таблицы ссылок, входящие в метаданные
Имя таблицы ссылок |
Описание |
AssemblyRef |
Содержит по одной записи для каждой сборки, на которую ссылается модуль. Каждая запись включает сведения, необходимые для привязки к сборке: ее имя (без указания расширения и пути), номер версии, региональные стандарты и маркер открытого ключа (обычно это небольшой хэш, созданный на основе открытого ключа издателя и идентифицирующий издателя сборки, на которую ссылается модуль). Каждая запись также содержит несколько флагов и хэш. Этот хэш служит контрольной суммой битов, составляющих сборку, на которую ссылается код. Среда CLR полностью игнорирует этот хэш и, вероятно, будет игнорировать его в будущем |
ModuleRef |
Содержит по одной записи для каждого РЕ-модуля, реализующего типы, на которые он ссылается. Каждая запись включает имя файла сборки и его расширение (без указания пути). Эта таблица служит для привязки моду ля вызывающей сборки к типам, реализованным в других модулях |
TypeRef |
Содержит по одной записи для каждого типа, на который ссылается модуль. Каждая запись включает имя типа и ссылку, по которой можно его найти. Если этот тип реализован внутри другого типа, запись содержит ссылку на соответствующую запись таблицы TypeRef. Если тип реализован в том же модуле, приводится ссылка на запись таблицы ModuleDef. Если тип реализован в другом модуле вызывающей сборки, приводится ссылка на запись таблицы ModuleRef. Если тип реализован в другой сборке, приводится ссылка на запись в таблице AssemblyRef |
MemberRef |
Содержит по одной записи для каждого члена (поля, метода, а так- же свойства или метода события), на который ссылается модуль. Каждая запись включает имя и сигнатуру члена и указывает на запись таблицы TypeRef, содержащую сведения о типе, определяющим этот член
|
На самом деле таблиц метаданных намного больше, чем показано в табл. 2.1 и 2.2, я просто хотел создать у вас представление об информации, на основании которой компилятор создает метаданные. Ранее уже упоминалось о том, что в состав метаданных входят также таблицы манифестов. О них мы поговорим чуть позже.
Метаданные управляемого РЕ-файла можно изучать при помощи различных инструментов. Лично я предпочитаю ILDasm.exe - дизассемблер языка IL. Для того чтобы увидеть содержимое таблиц метаданных, выполните следующую команду:
{codecitation class="brush: plain; gutter: true;" width="100%" }ILDasm Program.exe{/codecitation}
Запустится файл ILDasm.exe и загрузится сборка Program.exe. Для того чтобы вывести метаданные в читабельном виде, выберите в меню команду View -> Metalпfo -> Show (или нажмите клавиши Ctrl+M).
К счастью, ILDasm самостоятельно обрабатывает таблицы метаданных и комбинирует информацию, поэтому пользователю не приходится заниматься синтаксическим разбором необработанных табличных данных. Например, в приведённом фрагменте видно, что, показывая строку таблицы TypeDef, ILDasm выводит перед первой записью таблицы TypeRef определение соответствующего члена.
Не обязательно понимать, что означает каждая строка этого дампа, важно запомнить, что Program.exe содержит в таблице TypeDef описание типа Program. Этот тип идентифицирует открытый изолированный класс, производный от System.Object (то есть это ссылка на тип из другой сборки). Тип Program также определяет два метода: Main и .ctor (конструктор).
Метод Main- это статический открытый метод, чей программный код представлен на языке IL (а не в машинных кодах процессора, например х86). Main возвращает тип void и принимает единственный аргумент args - массив значений типа String. Метод-конструктор (всегда отображаемый под именем. ctor) является открытым, его код также записан на языке IL. Тип возвращаемого значения конструктора - void, у него нет аргументов, но есть указатель this, ссылающийся на область памяти, в которой должен создаваться экземпляр объекта при вызове конструктора.
Я настоятельно рекомендую вам поэкспериментировать с дизассемблером ILDasm. Он предоставляет массу сведений, и чем лучше вы в них разберетесь, тем быстрее изучите общеязыковую исполняющую среду CLR и ее возможности. В этой книге еще не раз будет использоваться дизассемблер ILDasm.