|
|
1.1. Что такое DLL?
DLL - это сокращение от Dynamic Link Library (динамически загружаемая библиотека).
С формальной точки зрения DLL - особым образом оформленный относительно независимый блок исполняемого кода. Особый способ оформления предполагает наличие в DLL так называемых секций импорта и экспорта. Секция экспорта указывает те идентификаторы объектов (функций, классов, переменных), доступ к которым предоставляет данная DLL. В этом случае мы говорим об экспортировании идентификаторов из DLL. В общем случае, именно секция экспорта предоставляет особый интерес для разработчиков. Хотя ничто не мешает реализовать DLL, которая не имеет данной секции, но, тем не менее, выполняет полезную работу. Относительная независимость связана с наличием/отсутствием секции импорта у DLL (т.е. секции, в которой описываются внешние зависимости данной DLL от других). Подавляющее большинство DLL (за исключением, быть может, DLL ресурсов) импортирует функции из системных DLL (kernel32.dll, user32.dll, gdi32.dll и др.). В большинстве случае при создании проекта в его опциях автоматически проставляется стандартный набор таких библиотек. Иногда в этот список необходимо добавить требующиеся для Ваших задач DLL (например, в случае использования библиотеки сокетов требуется дополнительно подключить библиотеку ws2_32.dll). "Исполняемый" код в DLL не предполагает автономного использования. Перед тем, как можно будет приступить к использованию, необходимо загрузить DLL в область памяти вызывающего процесса (т.е. DLL не может выполняться сама по себе - ей обязательно нужен клиент). Это явление носит название "проецирование DLL на адресное пространство процесса". И это не удивительно, если вспомнить тот факт, что процессор работает не только с регистрами, но и с адресами памяти. Поэтому каждому объекту DLL требуется свое место "под солнцем", чтобы иметь возможность быть выполненным при вызове. В конечном коде exe-файла, который генерирует компилятор, не будет инструкций процессора, соответствующих коду данной функции. Вместо этого будет сгенерирована инструкция вызова соответствующей функции (call). Так как DLL отображена на адресное пространство процесса, то код DLL будет легко доступен по call-вызову. Итак, формально, DLL - особым образом оформленный программный компонент, доступ к исполняемому коду которого приложение получает в момент старта (DLL неявной загрузки) или в момент использования (DLL явной и отложенной загрузки).
Что же касается физического представления на диске, то разница между dll- и exe-файлами небольшая. Как динамически линкуемые библиотеки, так и исполняемые модули приложений в Windows имеют формат Portable Executable (PE-файл), однако вы не можете "запустить" DLL-библиотеку на выполнение, как обычное приложение. "Узнать" DLL-файл можно по его сигнатуре (заголовку): признаком библиотеки является установленный флаг IMAGE_FILE_DLL (13-й бит) в поле Characteristics заголовка IMAGE_FILE_HEADER (эти константы описаны в файле winnt.h; подробная информация о формате exe- и dll-файлов - см. MSDN "Microsoft Portable Executable and Common Object File Format Specification"). Кроме того, в заголовках файлов динамически линкуемых библиотек указан нулевой размер стека - это связано с тем, что функции DLL используют стек вызывающего приложения. DLL-файл может содержать как инструкции (команды процессора), так и данные (разделяемые ресурсы). В общем случае, файл, являющийся динамически загружаемой библиотекой, не обязан иметь расширение .dll. Например, известные файлы *.cpl - это не что иное, как DLL, используемые апплетом панели управления; *.ocx - DLL, содержащие внутрипроцессные (inproc) COM-объекты.
Логическое (философское) представление DLL не имеет никаких ограничений. Удобно представлять себе DLL в виде сервера, который предлагает дополнительную функциональность Вашему приложению. Приложения, которые используют данную функциональность, являются клиентами DLL. Рисунок 1.1 показывает процесс взаимодействия приложения с DLL. После проецирования DLL на адресное пространство вызывающего процесса DLL становится частью этого процесса. Поэтому возможен абсолютно безболезненный вызов функций, экспортируемых DLL.
Рисунок 1.1. Взаимодействие приложения с DLL
1.2. Зачем нужны DLL?
1.2.1. Предпосылки к появлению DLL
Свою историю DLL ведут с средины 60-х годов прошлого столетия, однако по-настоящему широкое распространение динамически линкуемые библиотеки получили после появления операционной системы Windows.
Замечание: Крис Касперски ("Техника сетевых атак", 2001) пишет: "Именно в MULTICS (1965-69 гг.) появилась возможность динамического связывания модулей в ходе выполнения программы, более известная современному читателю по этим пресловутым DLL в Windows. Такой прием логически завершил эволюцию совершенствования оверлеев, обеспечив единый, унифицированный интерфейс для всех программ, позволяя сэкономить значительную часть оперативной памяти и процессорных ресурсов..."
По крайней мере, доподлинно известно, что операционные системы Windows 3.1/3.11 уже содержали программы, использующие DLL. В связи с тем, что в те времена емкости оперативной памяти и жесткого диска были значительно меньше, чем сейчас, использование DLL предоставляло ряд преимуществ:
- экономия дискового пространства за счет многократного использования кода (reusing). Если приложения используют один и тот же код, нет необходимости поставлять его в коде каждого приложения. Достаточно разработать DLL.
- экономия физической памяти (RAM) за счет загрузки в нее единственного экземпляра DLL. Именно тогда появились счетчики ссылок пользователей DLL - при каждом вызове функции ОС проверяет наличие загруженного в память экземпляра библиотеки. В случае положительного ответа счетчик ссылок пользователей данной DLL увеличивается на единицу. Если же экземпляр данной DLL в памяти не обнаружен, то операционная система загружает файл в память и присваивает счетчику значение "1". Механизм разделения кода носит название memory mapping (отображение в память). При выгрузке DLL из памяти уменьшается значение счетчика числа пользователей, в случае равенства его нулю DLL немедленно выгружается.
- изолирование и модификация кода DLL независимо от остального кода программы. Например, код визуализации изолируется от математической части. При изменении математического аппарата (например, при разработке нового, более быстрого алгоритма) перекомпиляция кода клиентского приложения (отвечающего за визуализацию результатов) не требуется. Этот фактор может играть значительную роль в том случае, если число клиентов достаточно велико.
Самое удивительное (время не стоит на месте!), что ранее политика Microsoft позволяла (и даже приветствовала) размещение DLL в системных директориях (таких как Windows\System, Windows\System32). Это порождало периодические проблемы конфликта версий при замене DLL (см. раздел "DLL Hell"). В связи с этим (а также с ростом емкости запоминающих устройств и соответствующим снижением цены на них) на данный момент политика Microsoft изменена на прямо противоположную - Microsoft настоятельно рекомендует хранить все используемые DLL в рабочем каталоге программы и лишь в случае острой необходимости пользоваться системными директориями.
1.2.2. Зачем нужны DLL сейчас?
Основные направления использования DLL:
- всевозможные модули расширения функциональности приложений - так называемые plug-in (см. пример с MatLab, Far и пр.);
- локализация приложения (подробнее об этом в разделе "DLL, содержащие только ресурсы");
- разделение объектов абстракции (функций, классов и пр.) между приложениями;
- независимость модификации кода - DLL может быть в любой момент переписана с сохранением экспортируемых интерфейсов;
- реализация определенных действий, которые можно совершить только при помощи DLL - см. раздел "Перехват API-вызовов";
- хранилище ресурсов с возможностью независимого изменения этих ресурсов.
Кстати, DLL широко используются в технологии COM (а до этого в OLE 1.0) - в качестве основы при построении так называемых inproc-серверов (внутрипроцессных серверов) используются DLL. Это позволяет упростить взаимодействие с приложением, благодаря загрузке используемых ActiveX объектов в адресное пространство клиента. В этом случае накладные расходы, связанные с преодолением границ адресных пространств при передаче данных (параметров функций и т.д.) - так называемый marshalling, сводятся к нулю. Все те абстракции, с которыми Вы работаете в повседневной программистской жизни, могут быть внедрены в DLL - классы, объекты, таймеры, потоки, функции и пр. Другое дело, что не всегда удобно и правильно работать с этими объектами вне DLL (см. например "Использование STL в DLL"). Связано это с тем, что не всегда логическое представление того или иного объекта может быть однозначно представлено (переведено) в физическое. Да-да, Вы не ослышались - использование DLL не налагает ограничений на используемый язык (точнее - почти не налагает!). Более того, как правило, DLL разрабатывается на другом языке программирования, нежели тот, который используется при ее загрузке. Приведем пример: разрабатывался математический проект, код которого реализовывался на M-языке среды MatLab (Matrix Laboratory). M-язык по своей природе является интерпретируемым языком программирования. После этого полученные алгоритмы были реализованы при помощи языка C++ и скомпилированы в DLL, которая также использовалась средой MatLab.
Таблица 1.1. Сравнение производительности исполнения кода (*.DLL vs *.M)
Задача - построение выпрямляющего функционала. Количество точек - 864.001 |
Среда подготовки исполняемого модуля Время исполнения, сек |
Visual Studio 7.0 |
Matrix Laboratory |
Функционал "Энергия" |
0,44 |
35,15 |
Функционал "Длина" |
0,22 |
34,05 |
Функционал "Регрессия" |
31,25 |
148,52 |
Таким образом, аналогичная реализация в скомпилированном варианте дает выигрыш по скорости исполнения от пяти до ста раз! И это не предел. Разумеется, подобная эффективность может быть очень хорошо использована при наличии соответствующих возможностей. Необходимо отметить, что DLL - мощный и удобный механизм и пользоваться им надо с умом. Впрочем, как и всегда.
1.3. Что лучше: один EXE-файл и никаких DLL или компактный EXE-файл и много DLL?
Вероятно известный вам Бьерн Страуструп в своей книге "Язык программирования C++" дает после каждой главы ряд полезных советов, один из которых звучит так: "Пользуйтесь здравым смыслом...". Этот же совет можно предложить и при использовании DLL в программе. Если вы считаете, что вам нужна DLL - сделайте ее, если нет других причин сделать иначе. Разумеется, не стоит каждую отдельную функцию размещать в DLL и затем экспортировать (хотя это и возможно). Со временем такой проект превратится в трудноуправляемого монстра, инициализация которого будет занимать порядочное время. Правда, экспортирование только одной функции из DLL может быть продиктовано следующей логикой: серверное приложение сканирует определенную директорию на предмет нахождения в ней файлов с определенным расширением. В случае нахождения такого файла сервер полагает, что это - DLL, которая экспортирует функцию с определенным именем (скажем, CplApplet или mexFunction), и, соответственно, он сможет ее загрузить. Таким простым образом возможно динамическое расширение функциональности приложения - именно такой подход проповедуется в случае Панели управления (Control Panel) и системы MatLab (Matrix Laboratory).
Не стоит также бояться использовать те плюсы, которые может принести использование DLL в программе (см. раздел "Что такое DLL"). Логически отделенная библиотека, часто используемая в совместных проектах - верный кандидат на размещение в DLL. Это позволит:
- поставлять различным клиентам приложения с различной функциональностью;
- модифицировать (оптимизировать, изменять) код DLL без повторной перекомпиляции клиентских приложений, использующих DLL. А это значит, что при модификации или исправлении ошибки достаточно изменить DLL (сама Microsoft часто именно так и делает - см. постоянно появляющиеся пакеты Service Pack);
- разделить разработку большого проекта на отдельные, независимые группы;
- обеспечить легкость отладки и тестирования. В этом случае возможности, связанные с использованием нисходящих технологий проектирования, существенно повышаются;
- использовать различные языки при написании DLL и клиентского приложения. Например, клиент может быть написан при помощи m-языка (тип интерпретируемого скрипта, используемый в системе MatLab), а DLL - при помощи языка C++. Это позволит провести дополнительную оптимизацию кода без потери его качества.
Разумеется, каждая DLL требует определенного процесса инициализации и, соответственно, затрат на него. В случае наличия большого количества DLL процесс инициализации может занимать порядочное время (правда, не обязательно все DLL инициализировать сразу - см. раздел "Как приложение загружает DLL", а также необязательно вообще инициализировать DLL - ведь иногда из DLL нужно получить не код, а находящиеся в ней ресурсы, - например, в случае использования DLL как хранилища локализованных строковых параметров - см. раздел " DLL, содержащие только ресурсы"). В случае помещения всего кода в exe-файл дополнительная инициализация не потребуется. Правда, при этом будет происходить постоянное дублирование кода. В случае единичного проекта это не составляет проблемы. Если же таких проектов несколько, возможности повторной перекомпиляции бывают существенно затруднены (и не эффективны!). Представьте себе, что было бы, если бы при каждом изменении Microsoft заставляла бы всех клиентов перекомпилировать "ядро" Windows?
Таким образом, здравый смысл - ваш верный помощник (как говорит один наш коллега - "Крайности вредны!"). Помните об этом. Кроме того, помните, что определенные проблемы проектирования без использования DLL (например, связанные с установкой ловушек - см. раздел "Внедрение DLL и перехват вызовов API-функций") решить вообще нельзя. Еще можно вспомнить так называемые ISAPI-расширения - высокоэффективные модули, используемые при написании Web -приложений. Ну, и, наконец, такая система как MatLab позволяет использовать собственноручно написанные и скомпилированные модули в виде DLL для повышения быстродействия работы Ваших программ. Так что использование DLL может быть продиктовано еще и вопросами оптимизации.
Подытоживая сказанное выше, можно сделать следующие выводы.
Случай использования одного большого EXE-файла:
- + относительная быстрота инициализации - каждая DLL требовала бы отдельного процесса инициализации;
- + все в одном файле (нет внешних зависимостей) - такой файл может поставляться без внешних библиотек;
- - постоянное дублирование кода (за счет увеличения размера *.exe-файла);
- - полная перекомпиляция при любом изменении;
- - все в одной куче: отсутствие разделения реализации логики различных (по функциональному наполнению) объектов;
- + идеально для небольших проектов (утилиты, тестовые приложения и пр.).
Случай разделения EXE и DLL:
- + физическое разделение логически-независимых объектов (классов, функций и пр.). Это позволяет проводить независимую разработку и последующее тестирование подобных DLL. На момент сбора объекты будут иметь достаточно предсказуемое поведение, чтобы быть сразу же использованными другими разработчиками;
- - возможно большее время загрузки (если не применять различного рода оптимизирующих действий - отложенную загрузку, управление предпочтительными базовыми адресами и пр.);
- + идеально для больших проектов (повторное использование кода + отсутствие его дублирования);
- + легкость update'а и замены.
А что использовать в каждом случае - это дело конкретной задачи.
2.1. Как приложение загружает DLL
Как вы уже знаете из предыдущих разделов, модуль DLL может определять два вида функций - внутренних (internal) и внешних (exported). Экспортируемые функции могут быть вызваны другими модулями (в т.ч. другими DLL). Внутренние функции могут быть использованы лишь кодом самой DLL. Динамически линкуемые библиотеки обеспечивают путь для модульного подхода при создании приложений. Кроме того, написанный однажды код может быть в дальнейшем использован другими приложениями. Любые вносимые изменения (не затрагивающие изменения внешнего интерфейса DLL) не будут отражаться на работе использующего данный код приложения.
Для того чтобы приложение могло использовать экспортируемые функции (либо экспортируемые переменные), DLL должна быть отображена (спроецирована, загружена) на адресное пространство использующего ее процесса. Существуют три основных способа проецирования DLL на адресное пространство, которые и будут рассмотрены ниже.
2.1.1. Неявная загрузка
При неявной загрузке DLL проецируется на адресное пространство вызывающего процесса (загружается) при его создании. Если по какой-либо причине неявная загрузка DLL завершается неудачно, загрузчик операционной системы немедленно прерывает процедуру создания процесса, выводит диалоговое окно для уведомления пользователя о возникшей проблеме и "прибивает" процесс. Сообщения об ошибке при этом для различных версий ОС выглядят немного по-разному - см. Рисунок 2.1 и Рисунок 2.2.
Рисунок 2.1. Сообщение об ошибке для Windows XP
Рисунок 2.2. Сообщение об ошибке для Windows 98
В случае неявной загрузки приложению требуются:
- h-файл (header - заголовочный) с прототипами функций, описаниями классов и типов, которые используются в приложении;
- lib-файл (library - библиотечный), в котором описывается список экспортируемых из DLL функций (переменных), и их смещения, необходимые для правильной настройки вызовов функций.
h-файл (*.h) требуется компилятору, чтобы определить суть (тип, список используемых параметров, их типы и пр.) имеющихся объектов. lib-файл (*.lib) требуется компоновщику (линкеру) для правильной настройки виртуальных адресов в приложении. Эти адреса являются смещениями относительно некоторого базового адреса, по которому DLL будет загружена в адресное пространство. В случае совпадения базового адреса и реального, по которому, собственно, DLL и будет размещена в памяти, дополнительная настройка адресов не требуется. Если же "не повезет" (адреса различаются), значит, при загрузке следует произвести модификацию адресов (этим занимается загрузчик) и лишь после этого продолжить выполнение приложения (см. раздел "Базовый адрес загрузки DLL"). Как вы понимаете, подобная модификация адресов отрицательно скажется на скорости загрузки и запуска приложения, использующего данную DLL.
Ну что ж, пора переходить от слов к делу. В предыдущем разделе мы создали DLL, в которой реализован уникальный алгоритм сложения двух целых чисел. Теперь создадим приложение, использующее созданную библиотеку при помощи неявной загрузки. Неявная загрузка позволяет на основе информации, прочитанной из *.lib-файла, связать идентификаторы клиентского приложения и DLL. Программист должен также предоставить компилятору заголовочный файл с определениями функций. Это позволит проверить корректность вызовов функций, а также сообщить, что данные функции должны экспортироваться.
Замечание: Зачем, спросите вы, еще и h-файл, если у нас есть lib-файл? Дело в том, что lib-файл не содержит информации (декорирование имен - не в счет!) относительно аргументов функции: их количестве, типах. Также нельзя узнать и тип возвращаемого значения. А эта информация и содержится в h-файле. Кроме того, указания h-файла заставляют компилятор "понять", какие идентификаторы нужно связывать посредством lib-файла.
Для построения клиентского приложения необходимо проделать следующие шаги:
Замечание: Здесь и далее мы будем использовать консольные приложения для демонстрации работы с DLL. Это позволит написать минимум кода, не усложняя и не запутывая его без лишней необходимости. Использование DLL в проектах других типов проводится аналогичным образом.
2.1.2. Явная загрузка
Этот способ связан с явным использованием основных функций Windows API из предложенного набора. В случае использования явной загрузки программист берет на себя львиную долю забот при работе с DLL.
Ниже перечислен наиболее часто используемый набор предоставляемого WinApi для работы с DLL явной загрузки:
- DisableThreadLibraryCalls - функция, "запрещающая" получать DLL уведомления DLL_THREAD_ATTACH и DLL_THREAD_DETACH (см. раздел "Зачем нужна функция DllMain?"). Это бывает полезно в многопоточных приложениях, когда постоянно создаются и уничтожаются рабочие потоки, а DLL не требует получения подобных уведомлений. В целях оптимизации исполняемого кода обычно вызывается в ответ на сообщение DLL_PROCESS_ATTACH.
- FreeLibrary - функция, используемая для явной выгрузки DLL из ОП. Используется для DLL, которая была перед этим загружена при помощи вызова LoadLibrary.
- FreeLibraryAndExitThread - функция, позволяющая потоку, созданному в коде DLL, быть безопасно уничтоженным (с последующей выгрузкой DLL).
- GetModuleFileName[Ex] - позволяют получить полный путь к конкретному модулю с идентификатором HMODULE.
- GetModuleHandle[Ex] - позволяют получить идентификатор HMODULE по имени модуля. Функция возвращает корректное значение HMODULE только для тех модулей, которые были спроецированы на адресное пространство вызывающего процесса.
- GetProcAddress - функция, позволяющая получить виртуальный адрес экспортируемой из DLL функции (или переменной) для ее последующего вызова.
- LoadLibrary[Ex] - позволяют спроецировать DLL на адресное пространство вызывающего процесса.
Основная нагрузка в этом случае ложится на функции LoadLibrary, LoadLibraryEx, FreeLibrary и GetProcAddress. Рассмотрим назначение основных функций.
Функции LoadLibrary и LoadLibraryEx
Данная функция предназначена для возможности проецирования указанной DLL (в качестве параметра принимается название файла DLL с расширением и, возможно, относительный/абсолютный путь к нему) на адресное пространство вызывающего процесса. В качестве имени файла также может использоваться имя не только dll-файла, но и, скажем, exe-файла. Это может быть полезным в том случае, если необходимо использовать ресурсы исполняемого файла - например, при помощи функций FindResource/LoadResource.
В случае успешности загрузки количество клиентов данной DLL увеличивается на единицу, и пользователю возвращается значение HMODULE загруженного модуля. Данный дескриптор может быть в дальнейшем использован в качестве параметра при использовании функций GetProcAddres и FreeLibrary. Возврат значения NULL свидетельствует о невозможности загрузки. Информацию об ошибке можно получить, используя функцию GetLastError. Дополнительные подробности вы можете посмотреть в стандартной справочной службе MSDN компании Microsoft.
Отметим несколько интересных моментов:
- небезопасно использовать LoadLibrary в функции DllMain (как вы помните, это функция инициализации DLL - см. раздел "Зачем нужна функция DllMain?"). Дело в том, что в момент инициализации данной DLL другие динамические библиотеки могут быть еще не спроецированы на адресное пространство процесса - и это может привести к взаимной блокировке. Будьте внимательны!
- получаемые дескрипторы не являются глобальными или наследуемыми. Каждый процесс должен самостоятельно вызывать LoadLibrary.
- при указании имени DLL без указания пути используется определенный алгоритм поиска - см. раздел "Порядок поиска файла DLL при загрузке".
- название имени файла DLL должно являться ANSI-строкой. Как уже упоминалось ранее в секции экспорта DLL все имена экспортируемых функций сохраняются в виде ANSI-строки. Убедиться в этом можно, если внимательно приглядеться к типу второго параметра функции GetProcAddress (см. пример ниже).
- в случае отсутствия расширения в имени DLL, подразумевается значение ".dll". Чтобы указать, что имя файла не имеет расширения, используйте ограничивающий символ '.', завершающий имя DLL.
- очень полезным (в целях оптимизации или каких-либо других) может быть использование флагов в функции LoadLibraryEx. Например, значение флага DONT_RESOLVE_DLL_REFERENCES заставляет ОС не вызывать функцию DllMain с различного рода уведомлениями. Флаг LOAD_LIBRARY_AS_DATAFILE позволяет загрузить DLL как обычный файл, не подготавливая к последующему выполнению кода в нем. Это бывает полезно в случае распаковки ресурсов из DLL. И, наконец, флаг LOAD_WITH_ALTERED_SEARCH_PATH заставляет использовать альтернативный путь поиска при загрузке DLL - см. раздел "Как изменить порядок поиска".
Функция GetProcAddress
Позволяет получить по имени функции необходимый виртуальный адрес для работы с ней. В случае невозможности получения адреса функция возвращает значение NULL. Таким образом, функция GetProcAddress - это второй необходимый шаг, который совершается для вызова функции из DLL в случае использования механизма явной загрузки. Далее мы рассмотрим пример кода, чтобы окончательно понять, как это происходит. Несмотря на то, что наиболее часто функция GetProcAddress используется для получения виртуального адреса какой-либо функции, тем не менее, она же используется и для получения адреса какой-либо переменной, экспортируемой из DLL. В разделе "Экспорт классов и переменных из DLL" мы рассмотрим, как можно осуществить данный процесс на практике.
Функция FreeLibrary
FreeLibrary вызывается на заключительном этапе работы с DLL. При этом происходит уменьшение счетчика клиентов данной DLL. В случае равенства его нулю DLL немедленно выгружается из памяти.
Таким образом, процесс работы с DLL в случае явной загрузки состоит из трех этапов:
- Загрузка DLL посредством вызова функции LoadLibrary. В результате обращения вызывающий процесс получает доступ к описателю загружаемой DLL (HMODULE), что позволяет обращаться к этой DLL в дальнейшем (до момента ее выгрузки - в случае последующей загрузки ей может быть присвоен совершенно другой дескриптор).
2.1.3. Отложенная загрузка
Этот вариант загрузки появился значительно позже своих "собратьев", описанных выше. Например, среда Visual Studio поддерживает данную функциональность, начиная с шестой версии. Если вы внимательно прочитали два предыдущих способа работы с DLL, то выяснили, что каждый из них обладает определенными достоинствами и недостатками.
Явная загрузка:
- + явное управление процессом жизни DLL;
- - перекладывание большей части работы по управлению DLL на программиста.
Неявная загрузка:
- + все заботы берет на себя компилятор и линкер;
- - ресурсы занимаются все время, а не на момент использования;
- - если вы работаете с ПО в "неизвестной" обстановке, то такая загрузка не всегда бывает полезной.
Приведем пример, чтобы пояснить последний пункт. Упоминавшаяся ранее среда системы MatLab обладает гибким интерфейсом с программами, написанными на С++ (причина этого должна быть ясна - достаточно взглянуть на таблицу эффективности исполнения кода в разделе "Что такое DLL", и все вопросы отпадут сами собой). При этом интерфейс основан на использовании DLL, которая экспортирует определенную функцию (mexFunction), на вход которой поступает специального типа параметры, полностью описывающие свой тип, размеры и количество. Это позволяет построить DLL таким образом, что требуемая функциональность будет использоваться как в программе, написанной на чистом C++ (компилируемый вариант), так и в программе, написанной на M-языке (интерпретируемый язык). При этом m-язык среды MatLab использует mexFunction для доступа к функциональности некоторой функции CalcXFunc, а C++ использует данную функцию напрямую (без посредников) - рисунок 2.3.
Рисунок 2.3. Взаимодействие с DLL приложений MatLab и C++
Но тут возникает вполне очевидная проблема. Дело в том, что для описания и последующего получения параметров в mexFunction используются специальные функции из набора MatLab API, которые, в свою очередь, заключены также в DLL. В программе на C++, естественно, эти функции не требуются (да и они вообще могут и не быть на компьютере пользователя). Не таскать же их с собой! DLL нельзя загрузить неявно - при старте приложения система постарается загрузить DLL из таблицы импорта, что (в случае отсутствия установленной версии MatLab) опять приведет к ошибке. Придется использовать явную загрузку, что, к сожалению, повлечет дополнительный труд со стороны программиста.
Что же делать? Ура! Выход есть! Использование механизма отложенной загрузки позволяет избежать подобных проблем. В этом случае необходимые в mexFunction функции определяются как связанные с DLL отложенной загрузки. Тогда программа, использующая интерфейс mexFunction, будет вынуждена иметь требуемые DLL. При первом вызове необходимой функциональности данные DLL будут загружены в память. Программа на C++, не имеющая представления о mexFunction, сможет спокойно работать с данной DLL, не требуя дополнительных функций MatLab API.
Ну что, мы вас убедили? Еще нет? Другой пример. Если программа предназначена для работы в различных версиях ОС, то, скажем, часть функций может появиться лишь в поздних версиях ОС и не присутствовать в ней на данный момент. Но ведь пока мы явно не обратимся к конкретной функции, DLL нам не нужна, и мы можем спокойно продолжать работу. В момент обращения к несуществующей функции пользователю можно выдать соответствующее предупреждение.
Теперь, думаем, мы вас точно убедили! Что же для этого нужно сделать, спросите вы? Ответим так - немного! Хотя кое-что действительно придется сделать в клиентском приложении, которое хочет загрузить DLL способом отложенной загрузки.
- Вызовы функции GetProcAddress для получения указателей на требуемые объекты. Особо отметим тот факт, что одна и та же функция используется как при работе с функциями, так и с переменными. Получение виртуального адреса функции может происходить как при помощи ANSI-строки, так и при помощи ее порядкового номера (но делать этого не рекомендуется - см. замечания ниже).
- Вызов функции FreeLibrary после завершения всех требуемых действий с объектами данной библиотеки. В результате этого освобождается место в ОП, проводятся действия по деинициализации DLL. Разумеется, если вы забудете вызвать данную функцию при завершении приложения, за вас это сделает система. Но лучше всегда все делать самому! Этим самым вы в дальнейшем сможете избежать лишних неприятностей. Также полезно проверять значение, возвращенное функцией FreeLibrary - функция вернет FALSE, если закрытие описателя HMODULE невозможно. Наиболее вероятной причиной может являться то, что дескриптор уже был по ошибке закрыт где-либо в другом месте программы.
Итак, вот общий алгоритм работы DLL с отложенной загрузкой (все эти действия совершаются абсолютно прозрачно для программиста):
- Вызов вспомогательной функции (helper-функции - функции-переходника) вместо той, которая указана на самом деле. Адрес этой функции находится в специальной таблице, похожей на структуру IAT (и называется она pseudoIAT). При компоновке кода линкер заполняет pseudoIAT адресами функций-переходников.
- Проверить - загружена ли необходимая DLL? Если библиотека еще не загружена, то загрузить ее, вызвав LoadLibrary.
- Вызвать GetProcAddress с именем необходимой функции.
- Вызвать функцию по полученному адресу, запомнив его для последующих вызовов. Осуществляется это в функции __delayLoadHelper (см. файл DelayHlp.cpp). Эта же функция занимается тем, что на место вызова функции-переходника в pseudoIAT подставляет адрес правильной функции, поиск которой был осуществлен в предыдущем пункте. Таким образом, все последующие обращения происходят уже напрямую.
- При выгрузке DLL отложенной загрузки pseudoIAT вновь инициализируется первоначальными значениями, так что повторный вызов функции приведет к выполнению алгоритма с п.1.
2.2. Типичные проблемы при работе с DLL
Какие подводные камни могут подстерегать ничего не подозревающего разработчика на пути использования DLL? Наиболее часто возникающие вопросы по этой теме рассмотрены именно в этой главе. Если какой-то материал рассмотрен в другом разделе, то здесь Вы найдете соответствующие ссылки.
Замечание: Список изложенных проблем далеко не является исчерпывающим. Так что, если Вы усвоите материал этой главы, это не значит, что проблем у Вас не будет. Эта информация носит познавательный характер и не претендует на полноту!
Проблема 1. Почему моя DLL не загружается?
Такой вопрос нередко задают начинающие программисты. Должны признаться, что мы также неоднократно ломали голову над этой проблемой. Почему функция LoadLibrary вместо правильного значения HINSTANCE для загружаемой DLL возвращает NULL, хотя файл DLL гарантированно находится в текущем каталоге программы и доступен для загрузки?
Ничего таинственного в этом нет. Вспомним еще раз, как система загружает DLL: загрузчик операционной системы отыскивает файл DLL и подключает (проецирует) его к адресному пространству загружающего процесса. Затем загрузчик просматривает секцию импорта загружаемой DLL и извлекает из нее имена библиотек, неявно загружаемых (implicit linking) данной DLL, и пытается подключить их к адресному пространству процесса, затем просматривает секции импорта этих DLL, и так далее. Процесс продолжается рекурсивно до тех пор, пока весь необходимый код и данные не окажутся успешно спроецированы на адресное пространство процесса. Если в какой-то момент загрузчик не сможет найти хотя бы одну из необходимых DLL, то немедленно прервет загрузку и отключит от адресного пространства процесса все загруженные модули, а функция LoadLibrary вернет значение NULL, сигнализируя о неуспешной загрузке.
Можно потерять не один час, пытаясь разобраться, почему не загружается DLL при запуске на одной машине, хотя на другой машине эта же DLL может загружаться успешно. Как правило, проблема заключается в следующем. Приложение загружает DLL-библиотеку, которая неявно (implicit linking) загружает другую DLL, а та, в свою очередь, неявно загружает одну из библиотек системного назначения (например, Msvcp60.dll). Вот эта-то библиотека и отсутствует на одной машине и присутствует на другой. Для решения проблемы необходимо включить требуемую DLL в дистрибутив программы.
Чтобы избежать этой неприятности, используйте утилиту Dependency Walker (depends.exe) из Platform SDK для отслеживания взаимозависимостей между загружаемыми модулями либо утилиты аналогичного назначения других фирм. В дистрибутив Вашей программы необходимо включить ВСЕ необходимые приложению DLL.
Проблема 2. Несовместимость интерфейсов используемых функций
Представим себе другую ситуацию: мы имеем DLL, которую мы хотим использовать в своей программе. Эта DLL великолепно отлажена, задокументирована, написана в такой же среде, в которой мы собираемся писать программу, имеются снабженные комментариями на русском языке заголовочные файлы, которые без проблем включаются в текст программы. Еще лучше - есть исходники библиотеки. А еще лучше - мы сами эту библиотеку и написали...
Размечтались? А теперь пора вернуться к действительности!
На самом деле, чаще всего бывает как раз наоборот: документация к библиотеке неполная или вообще отсутствует, заголовочных файлов нет, исходников нет и т.д. Основной проблемой загрузки библиотеки в таком случае становится определение и корректная декларация параметров функций (что будет в случае их несоответствия? Об этом можно узнать в разделе "Разработка и использование DLL в различных средах"). Проблема состоит еще и в том, что типы данных в различных языках и даже в различных реализациях одного языка отличаются. В этой ситуации выходом может быть использование типов WinAPI -библиотек, таких как BYTE, WORD, DWORD, PVOID и т.д. Такие типы, как правило, корректно поддерживаются во всех средах, поддерживающих библиотеку функций WinAPI.
Проблема 3. Декорирование имен
Не менее актуальна и проблема декорирования имен, когда исходное имя функции при экспортировании искажается с целью сохранения дополнительной информации об этом имени (причем правила декорирования меняются от компилятора к компилятору). Разработчик не всегда об этом помнит, а потому пытается использовать то имя, которое было определено в исходном коде DLL. Вы уже имели счастье наблюдать этот процесс в действии при прочтении предыдущего раздела - при использовании функции getSum в VC++ нам пришлось писать что-то вроде ?getSum@@YAHHH@Z. В противном случае линкер не может связать необходимые имена, о чем сообщает в виде ошибки "Неразрешимая внешняя ссылка". Как избавиться от декорирования более подробно изложено в разделе "Декорирование имен".
Проблема 4. Конфликт версий
Другой распространенной проблемой при загрузке DLL является конфликт версий. Как определить, что требуемая DLL имеет нужную версию, и что в ней содержится требуемая функция? Частичным выходом в такой ситуации является использование ресурса VERSIONINFO для определения версии библиотеки. Подробнее об этом можно почитать в разделе "DLL Hell".
Проблема 5. Конфликты базовых адресов
Среди технических проблем загрузки DLL следует отметить конфликты базовых адресов библиотек. Дело в том, что у каждой динамически компонуемой библиотеки есть предпочтительный базовый адрес. Это адрес памяти, по которому DLL будет загружена в кратчайший срок. Для просмотра предпочтительных базовых адресов программы используется утилита DUMPBIN.EXE из пакета Visual Studio с ключом /HEADERS или другие утилиты аналогичного назначения (например, TDUMP.EXE компании Borland). Если несколько библиотек претендуют на один и тот же адрес, то только одна из них загружается по заданному базовому адресу, все остальные же будут загружены по другим базовым адресам, при этом время загрузки сильно увеличивается - немногие об этом знают и, тем более, задумываются!
Замечание: По умолчанию, большинство компоновщиков устанавливают базовый адрес загрузки DLL в 0x10000000. Можно поспорить, что сегодня как минимум половина библиотек DLL в мире пытается загрузиться по этому адресу.
Для того чтобы сократить время загрузки, следует изменить предпочтительные адреса библиотек таким образом, чтобы они не перекрывали друг друга. Для этого подойдет утилита REBASE.EXE из пакета Visual Studio. Еще раз повторим, что если Вы не позаботились о разнесении предпочтительных базовых адресов используемых библиотек, то в этом случае эту работу проделает за Вас загрузчик. Правда, сделать он это сможет, если Вы при компиляции библиотеки сохранили раздел переадресации (reallocation table). Если же такой раздел не будет сохранен (в целях уменьшения размера DLL), то загрузка динамической библиотеки приведет к ошибке. Кстати, Джеффри Рихтер разработал полезную функцию в приложении ProcessInfo.exe, которая позволяет отслеживать какие DLL были загружены по их предпочтительным адресам, а какие - нет!
Проблема 6. Пути поиска DLL
Как правило, это оборачивается проблемой при связывании DLL (в случае неявной загрузки) - в этом случае на экране появляется сообщение об ошибке, информирующее о невозможности найти требуемую DLL (в случае использования явной загрузки LoadLibrary возвращает значение NULL). Подробнее об этом можно почитать в разделе "Алгоритм загрузки DLL". Также это касается момента отладки - когда среда "не видит" DLL - в этом случае может быть установлена неправильная директория по умолчанию либо DLL расположена не в том каталоге, который требуется.
Проблема 7. Инициализация DLL
DLL может содержать специальную функцию DllMain, которая в общем случае вызывается при отображении динамической библиотеки на адресное пространство. Так вот, если в процессе исполнения DllMain вернет значение FALSE, то попытка загрузки библиотеки закончится неудачей (в случае явной загрузки вызов функции LoadLibrary вернет значение NULL). Информация о назначении и использовании функции DllMain находится в разделе "Зачем нужна функция DllMain?".
Проблема 8. Связывание адресов
Если провести предварительное связывание адресов (в целях оптимизации загрузки DLL), то установка любого service pack (пакета обновлений) приведет к необходимости повторного связывания адресов, о необходимости которого можно легко забыть. Что такое связывание? Это процесс, позволяющий "заранее" вычислить виртуальные адреса всех импортируемых в приложение функций (такая операция проводится только один раз - например, при установке приложения). Вычисленный адрес сохраняется в специальной таблице импортируемых адресов (IAT - import address table) вашего модуля. Поэтому системному загрузчику не требуется вычислять эти адреса каждый раз при старте приложения, что позволяет существенно ускорить загрузку такого приложения (особенно в том случае, когда оно использует много DLL). Связывание можно выполнить вручную выполнением утилиты bind.exe, но мы настоятельно не рекомендуем этого делать, поскольку стабильность работы системы и/или прикладных программ при установке любого пакета обновления ОС будет катастрофически нарушена, и весь процесс связывания для каждого приложения придется производить заново.
Проблема 9. Использование отложенной загрузки в VC++ 6.0
Проблема с DLL отложенной загрузки в VC++ 6.0. была подробно разобрана в предыдущем подразделе (см. "Как приложение загружает DLL").
Проблема 10. Ошибка выгрузки DLL отложенной загрузки
В случае использования отложенной загрузки возможна неправильная установка ключей при компиляции EXE-приложения. Например, если "забыть" указать ключ /DELAY:UNLOAD, то любые попытки выгрузить DLL из ОП при помощи __FUnloadDelayLoadedDLL/__FUnloadDelayLoadedDLL2 закончатся неудачей (и возвратом значения FALSE). Кроме этого, параметр, передаваемый в функцию выгрузки DLL отложенной загрузки из памяти, должен в точности (вплоть до указанного расширения) совпадать с тем, что был указан в параметре ключа /DELAYLOAD.
Проблема 11. Несоответствие моделей управления памятью
Более тонкие ошибки происходят в связи с несоответствием в моделях управления памятью. Как правило, такие ошибки связаны с тем, что динамически выделенная память размещается в разных хипах (локальных кучах) - например, ее выделение происходит в контексте DLL, а освобождение - в контексте приложения. К чему это может привести? В общем случае - к полному краху вашего приложения (а если особенно "повезет", то изредка появляющимися ошибками). Почему такие ошибки возникают? Дело в том, что различные языки программирования используют различные стандарты и принципы размещения и освобождения памяти в куче - например, известно, что Pascal предполагает резервирование четырех байт, предшествующих возвращенному адресу участка кучи, в которых записывается размер выделенного фрагмента. Если память выделена и освобождена кодом, сгенерированным одним компилятором, то, скорее всего, все пройдет неплохо (хотя и здесь есть свои сложности, связанные с наличием в памяти различных версий RTL (runtime library), которые также могут использовать различные варианты выделения памяти). А если нет? Подробнее на эту тему можно почитать в разделе "Разработка и использование DLL в различных средах" и "Использование STL в DLL".
Что такое .NET Framework?
Microsoft .NET Framework - это платформа для создания, развертывания и запуска Web-сервисов и приложений. Она предоставляет высокопроизводительную, основанную на стандартах, многоязыковую среду, которая позволяет интегрировать существующие приложения с приложениями и сервисами следующего поколения, а также решать задачи развертывания и использования интернет-приложений. .NET Framework состоит из трех основных частей - общеязыковой среды выполнения (common language runtime), иерархического множества унифицированных библиотек классов и компонентную версию ASP, называемую ASP.NET.
Наверх
Runtime Technical Questions
Терминология
Что такое общеязыковая среда выполнения - Common Language Runtime (CLR)?
Общеязыковая среда выполнения - это ядро для выполнения приложений в .NET Framework
Она предоставляет набор сервисов, включая следующие:
- управление кодом (загрузка и выполнение)
- изоляция памяти приложений
- проверка безопасности типов
- преобразование промежуточного языка в машинный код
- доступ к метаданым (расширенная информация о типах)
- управление памятью для управляемых объектов
- проверка безопасности кода
- обработка исключений, включая межъязыковые исключения
- взаимодействие между управляемым кодом, COM-объектами и существующими DLL(неуправляемый код и данные)
- поддержка сервисов для разработки (профилирование, отладка и т.д.)
Наверх
Что такое общая система типов(CTS)?
Общая система типов - это мощная система типов, встроенная в CLR, которая поддерживает типы и операции, существующие в большинстве языков программирования.
Наверх
Что такое общеязыковая спецификация - Common Language Specification (CLS)?
Common Language Specification - это набор конструкций и ограничений, которые являются руководством для создателей библиотек и компиляторов. Она позволяет библиотекам быть полностью использованными из любого языка программирования, поддерживающего CLS, и позволяет этим языкам интегрироваться друг с другом. CLS является подмножеством общей системы типов. CLS очень важна для разработчиков, которые пишут код, который будет использоваться другими разработчиками. Когда разработчик проектирует публично доступное API, используя правила CLS, то это API можно будет легко использовать из любого языка программирования, управляемого CLR.
Наверх
Что такое промежуточный язык Microsoft - Microsoft Intermediate Language (MSIL)?
MSIL - это независимый от процессора набор инструкций, в который компилируются программы в .NET Framework. Он содержит инструкции для загрузки, хранения, инициализации и вызова методов объектов.
Вместе с метаданными и общей системой типов, MSIL делает реальной межъязыковую интеграцию.
Перед выполнением, MSIL преобразуется в машинный код. Он не интерпретируется.
Наверх
Что такое управляемый код (managed code) и управляемые данные (managed data)?
Управляемый код - это код, который работает в среде CLR. Чтобы выполняться в среде, код должен предоставить определенный уровень информации (метаданных) для среды выполнения. Код C#, Visual Basic .NET, и JScript .NET является управляемым по умолчанию. Код Visual Studio .NET C++ не является управляемым по умолчанию, но компилятор может создавать управляемый код, для этого нужно указать аргумент в командной строке(/CLR).
Близким понятием к управляемому коду является управляемые данные - данные, которые создаются и уничтожаются сборщиком мусора CLR. Данные C#, Visual Basic и JScript .NET являются управляемыми по умолчанию. Но данные C# могут быть помечены как неуправляемые, используя специальное ключевое слово. Данные Visual Studio .NET C++ являются неуправляемыми по умолчанию (даже при использовании флага /CLR ), но при использовании Managed Extensions for C++, класс может быть помечен как управляемый, используя ключевое слово __gc. Как можно понять из имени, это означает, что память для экземпляров данного объекта должна управляться сборщиком мусора. Тогда класс становится полноценным участником сообщества .NET Framework, с теми преимуществами и ограничениями, которые она дает. Например, преимуществом является корректное взаимодействие с классами, написанными на других языках программирования, а ограничением явялется возможность наследования только от одного базового класса.
Наверх
Сборки
Что такое сборка?
Сборка - это базовый строительный блок приложения в .NET Framework. Это набор функциональности, которая создается, развивается и развертывается, как единый модуль (при этом он может содержаться в одном или нескольких файлах). Все управляемые типы и ресурсы помечаются либо как доступные только внутри сборки, либо как доступные вне модуля.
Составной частью сборки является декларация, которая позволяет сборке быть самоописанной. Декларация
- идентифицирует сборку (в виде текстового имени), версию, культуру и цифровую сигнатуру(если сборка разделяется среди приложений).
- определяет входящие в состав файлы (по имени и хэшу) .
- указывает типы и ресурсы, существующие в сборке, включая описание тех, которые экспортируются из сборки
- перечисляет зависимости от других сборок
- указывает набор прав, необходимых сборке для корректной работы
Эта информация используется в период выполнения, чтобы разрешить ссылки, обеспечить корректное использование версий, проверить целостность загруженных сборок. Среда выполнения может определить сборку для любого запущенного объекта, поскольку любой тип используется только в контексте сборки. Сборки также являются модулями, для которых применяются проверки безопасности доступа кода. Идентификация производится для каждой сборки поотдельности, чтобы определить, какие права можно предоставить коду, который она содержит.
Самоописываемая природа сборок позволяет реализовать безболезненную инсталляцию и развертывание, через XCOPY.
Наверх
Что такое приватные сборки и совместно используемые сборки?
Приватные сборки используются только одним приложением, и они хранятся в директории, в которой установлено приложение или ее поддиректории. Совместно используемые сборки могут быть использованы несколькими приложениями. Чтобы предоставить сборку в совместное использование, необходимо дать сборке уникальное шифрованное "строгое имя". В отличии от совместно используемой сборки, имя приватной сборки должно быть уникально только в рамках приложения.
Вводя разделение между приватными и совместно используемыми сборками, мы делаем понятие совместного использования осознанным решением. Просто разместив приватную сборку в директорию приложения, вы будуте уверены в том, что эта сборка будет работать только в этом приложении. Ссылки на приватные сборки разрешены только локально, в рамках приложения.
Есть несколько причин, по которым вы можете решить создавать и использовать совместно используемые сборки, например, возможность использования версионной политики. Тот факт, что совместно используемая сборка имеет зашифрованное строгое имя означает, что только автор сборки имеет ключ, чтобы создать новую версию сборки. Таким образом, если вы напишите правило, что вы хотите использовать новые версии сборки, то вы будете иметь определенную гарантию, что обновление версии создано и проверено ее автором. В противном случае вы не примете ее.
Для локально устанавливаемых приложений, совместно используемые сборки обычно устанавливаются в глобальный кэш сборок. Ключевой особенностью системы управления версиями .NET Framework является то, что загруженный код не влияет на выполнение локально установленных приложений. Загруженный (с Интернета или Интранета) код размещается в специальный кэш загрузки и не доступен глобально на машине, даже если определенные из компонент помечены как совместно используемые сборки.
Классы, которые включены в состав .NET Framework являются совместно используемыми сборками.
Наверх
Если я хочу создать совместно используемую сборку, требует ли это работы с подписями и парами ключей?
Создание совместно используемой сборки включает работу с ключами шифрации. Для построения сборки обязательно необходим только публичный ключ. Компиляторы, входящие в .NET Framework, имеют набор аргументов в командной строке, которые позволяют указать публичный ключ при создании сборки. Обычно копию ключа хранят вместе с исходниками и указывают, что при сборке использовать данный ключ. Перед передачей сборки, она должна быть полностью подписана, с соответствующим приватным ключом. Это делается с использованием утилиты SN.EXE, входящей в состав SDK.
Подписи строгих имен не включают сертификаты, как например, делает Authenticode. Нет необходимости подключать сторонние огранизации, не нужно дополнительных плат и сертификатов. Кроме того, накладные расходы для проверки строгого имени значительно меньше, чем в Authenticode. Однако при работе со строгими именами не делается никаких проверок о доверии определенному издателю. Строгие имена позволяют вам быть уверенными, что содержимое сборки не изменено и что загруженная сборка получена от того же производителя. Но не делается никаких предположений относительно того, можете ли вы доверять данному издателю.
Наверх
В чем отличие между пространством имен и именем сборки?
Пространство имен - это логическая схема именования типов, в которой простому имени типа, например, МойТип, предшествует разделенное точками иерархическое имя. Такая схема именования находится полностью под контролем разработчика. Например, по имени типов МояКомпания.ФайловыйДоступ.А и МояКомпания.ФайловыйДоступ.В можно ожидать, что их функциональность имеет отношение к работе с файлами. .NET Framework использует иерархическую схему именования типов для объединения типов в логические категории с общей функциональностью. Средства разработки позволяют удобно просматривать типы, ссылаться на них и использовать в коде. Концепция пространства имен не имеет прямого отношения к сборке. Одна сборка может содержать типы, имена которых начинаются в разных пространствах имен, и одно пространство имен может быть реализовано в нескольких сборках. В .NET Framework пространство имен является соглашением по именовании в процессе разработки, в то время как сборка устанавливает область действия имени для типа в период выполнения.
Наверх
Развертывание и изоляция приложений
Какие существуют варианты развертывания моего .NET приложения?
.NET Framework упрощает развертывание, делая возможным установку системы без побочных эффектов и развертывать приложение, используя утилиту XCOPY. Поскольку все запросы обрабатываются сначала в приватной директории приложения, то для запуска приложения необходимо просто скопировать файлы приложения на диск. Никакой регистрации не требуется.
Такой сценарий применим для Web-приложений, Web-сервисов и клиентских приложений. Однако, сценарий с использованием XCOPY, не всегда подходит при распространении приложения. Например, когда приложение состоит из небольшой части приватного кода, и использует большое количество совместно используемых сборок, или когда приложение не всегда полностью установлено локально (например, определенные части приложения скачиваются и загружаются по требованию). Для таких ситуаций, в .NET Framework предусмотрены сервисы для скачивания кода и интеграция с Windows Installer. Скачивание кода, которое поддерживается .NET Framework, предоставляет ряд дополнительных преимуществ, таких, как поддержка скачивания по частям, безопасности доступа кода (без Authenticode), изоляция приложений (код, скаченный в рамках одного приложения, не оказывает влияние на другое). Windows Installer представляет собой еще один мощный механизм развертывания, доступный для .NET приложений. Все возможности Windows Installer, включая публикацию, размещение рекламных заставок, восстановление приложений, доступны для .NET приложений в Windows Installer 2.0.
Наверх
Я написал сборку, которую хочу использовать в нескольких приложениях. Где я должен разместить ее?
Сборки, которые будут использоваться несколькими приложениями, размещаются в глобальном кэше сборок. В версиях PreRelease и Beta, используйте переключатель /i в утилите GACUtil из SDK, чтобы установить сборку в кэш:
gacutil /i myDll.dll
Windows Installer 2.0, входящий в состав Windows XP и Visual Studio .NET будет иметь возможность устанавливать сборки в глобальный кэш сборок.
Наверх
Как я могу посмотреть, какие сборки установлены в глобальном кэше сборок?
В .NET Framework входит расширение оболочки Windows для работы просмотра кэша сборок. Перейдите в директорию % windir%\assembly, используя проводник, чтобы запустить программу просмотра.
Наверх
Что такое домен приложения?
Домен приложения (сокращенно AppDomain) - это виртуальный процесс, который используется для изоляции приложения. Все объекты, созданные в рамках одного приложения (другими словами, любая последовательность создания объектов, которая начинается в приложении), создаются в рамках определенного домена приложения. Несколько доменов приложений могут существовать в одном процессе операционной системы, предоставляя легкий способ для изоляции приложения.
Процесс операционной системы обеспечивает изоляцию, получая выделенное адресное пространство. Это эффективно, но и дорого, и не масштабируемо для требований, предъявляемый для больших Web-серверов. CLR реализует изоляцию приложений, управляя памятью, использованной кодом, запущенным в рамках домена приложения. Это гарантирует, что не будет обращений в память, находящуюся все рамок домена. Важно отметить, что только безопасный код может управляться таким образом (среда выполнения не может гарантировать изоляцию, когда в домен приложения загружен небезопасный код).
Наверх
Сборка мусора
Что такое сборка мусора?
Сборка мусора - это механизм, позволяющий компьютеру определить, когда объект более недоступен. Тогда он автоматически освобождает память, используемую этим объектом (вызывая функцию finalizer, реализованную пользователем). Некоторые сборщики мусора (в т.ч. используемый в .NET), "сжимают" память, уменьшая количество ресурсов, используемых вашей программой.
Наверх
Как наличие сборщика мусора влияет на написание кода?
Для большинства программистов, наличие сборщика мусора (и использование его объектов) означает, что они больше не должны заботиться об освобождении памяти, подсчете ссылок на объекты, даже когда они используют сложные структуры. Однако это может потребовать изменения в стиле программирования, например, когда вы обычно освобождаете системные ресурсы (файловые указатели, блокировки и т.д.) в том же участке кода, где и освобождаете память для объекта. При наличии сборщика мусора, вы должны реализовать метод, который освобождает системные ресурсы (которые находятся под управлением вашей программы), и позволить сборщику мусора освободить память.
Наверх
Могу я не использовать память, контролируемую сборщиком мусора?
Все языки, поддерживаемые средой разработки, позволяют создают класс в памяти, контролируемой сборщиком мусора. Это дает преимущества в части более быстрой выделении памяти, а также позволяет разработчику избежать работы по освобождению каждого объекта.
В CLR также поддерживается понятие Тип-Значение (ValueType) - понятие, подобное классам, за исключением того, что такие значения размещаются на стеке (а не в куче), поэтому автоматически удаляются, когда завершается выполнение процедуры, в которой они определены. Таким образом реализуются структуры в C#.
Managed Extensions для C++ позволяют вам указать, где создавать объекты. Если вы объявляете управляемый класс, указывая ключевое слово __gc, то он будет размещен в куче памяти, контролируемой сборщиком мусора. Если вы не использовали ключевое слово __gc, то поведение будет аналогично обычным объектам C++, которые создаются в куче памяти C++ и освобождаются, используя метод "free".
Для дополнительной информации о сборке мусора, см.:
Удаленные вызовы
Как взаимодействия внутри процесса и между процессами работают в CLR?
Есть два аспекта взаимодействия внутри процесса: между контекстами внутри одного домена приложения и между разными доменами приложений. Между контекстами внутри одного домена приложения, используются прокси. Нет маршаллинга и сериализации. Когда взаимодействие происходит между доменами приложений, то происходит маршаллинг и сериализация, используя бинарный протокол среды выполнения.
Межпроцессные взаимодействия используют блочный (pluggable) канал и протокол формата, каждый для определенных целей.
- Если разработчик указывает конечную точку, с использованием утилиты soapsuds.exe для создания прокси, то HTTP канал и формат SOAP используются по умолчанию.
- Если разработчик делает вызов в управляемом коде, то необходимо точно указать, какой канал и какой формат использовать. Это может быть определено административно, через файлы конфигурации, либо через вызовы API для загрузки определенных каналов. Есть следующие варианты:
HTTP канал с форматом SOAP (HTTP работает хорошо в Интернет, или в любом месте, где трафик должен идти через системы защиты)
TCP канал с бинарным форматом (TCP - более производительный вариант для локальных сетей)
Когда происходит переключение между управляемым и неуправляемым кодом, то используется инфраструктура COM/DCOM. В начальных вариантах CLR, он же использовался для вызова сервисных компонент (компонент, которые используют COM+ сервисы). В окончательном варианте, должна быть реализована возможность настраивать вызов любой удаленной компоненты.
Сборка мусора для арспределенных объектов, носит название "leased based lifetime." - договорной жизненный цикл. Каждый объект имеет оговоренное время, и когда это время истекает, то объект отсоединяется от инфраструктуры CLR. Объекты имеют по умолчанию время обновления - срок обновляется, когда происходит удачный вызов от клиента к объекту. Кроме того, клиент может непосредственно обновить договор.
Совместимость
Могу я использовать COM-объекты из программы в .NET Framework?
Да. Любая COM-компонента, которую вы создаете сегодня, может быть использована из управляемого кода, и в общем случае адаптация происходит автоматически.
COM-компоненты доступны из .NET Framework через обертку вызовов среды выполнения - runtime callable wrapper (RCW). Эта обертка оборачивает интерфейсы вызываемых COM-компонент в интерфейсы, совместимые с .NET Framework. Для OLE-интерфейсов, обертка может создаваться автоматически из библиотеки типов. Для других интерфейсов, разработчики могут написать свою обертку и вручную связать типы компоненты с типами, совместимыми с .NET Framework.
Могут компоненты .NET Framework использоваться из COM-программ?
Да. Управляемые типы, которые вы создаете, могут быть доступны из COM и в общем случае все происходит автоматически. Некоторые возможности управляемого окружения не доступны из COM. К примеру, статические методы и параметризованные конструкторы не могут быть использованы из COM. В общем случае, бьыло бы хорошо заранее решить, кто будет пользователем данного типа. Если тип будет использоваться из COM, то вы можете быть ограничены в использовании некоторых возможностей.
В зависимости от языка, который вы используете для создания управляемого типа, он может быть видимым или невидимым по умолчанию.
Компоненты .NET Framework доступны из COM, используя обертку вызовов COM - COM callable wrapper (CCW). Она подобна RCW (см. предыдущий вопрос), но работает в противоположном направлении. И также, если средства разработки .NET Framework не могут автоматически создать обертку, или автоматическое поведение не соответствует тому, что вы ожидали, то вы можете создать обертку вручуню.
Можно использовать Win32 API из программы в .NET Framework?
Да. Используя платформный вызов, программы .NET Framework могут получить доступ к базовым библиотекам, указывая точки входа в DLL.
Вот пример вызова из C# функции MessageBox :
using System; using System.Runtime.InteropServices; class MainApp { [DllImport("user32.dll", EntryPoint="MessageBox")] public static extern int MessageBox(int hWnd, String strMessage, String strCaption, uint uiType); public static void Main() { MessageBox( 0, "Hello, this is PInvoke in operation!", ".NET", 0 ); } }
Безопасность
Как сделать, чтобы мой код взаимодействовал с системой безопасности?
Обычно для этого ничего не нужно делать, большинство приложений запускаются как безопасные и недоступны для многих атак. Просто необходимо использовать стандартные библиотеки для доступа к ресурсам (таким, как файлы) или использовать защищенные операции (приватные члены типа), поддержка безопасности уже включена в эти библиотеки. Есть одна простая вещь, которую разработчик может захотеть включить - это описать запрашиваемые права, чтобы ограничить права, которые может получить его код (если это требуется). Одновременно это дает уверенность в том, что если код запущен, то он получит все права, которые ему нужны.
Только разработчики, которые создают новые базовые библиотеки классов, которые используют новые типы ресурсов, должны непосредственно работать с системой безопасности.
Почему мой код получает исключение от системы безопасности, когда я запускаю ее с сетевого диска?
Используемая по умолчанию политика безопасности дает ограниченный набор прав для кода, который запускается из локальной интранет зоны. Эта зона определяется установками безопасности Internet Explorer, и используется, чтобы определить локальную сеть в предприятии. Поскольку файлы, имеющие UNC-вид, или находящиеся на сетевом диске (используя команду NET USE), передаются через локальную сеть, они тоже считаются в локальной зоне интранет.
Значения по умолчанию исходят из ситуации небезопасного интранета. Если ваш интранет безопасен, то вы можете изменить политику безопасности (используя утилиту конфигурации .NET Framework или утилиту CASPol ), чтобы предоставить больше прав для локального интранета, или его части (например, для определенных разделяемых дисков).
Как запустить код, если политика безопасности его остановила?
Исключения по безопасности случаются, когда код пытается выполнить операции, на которые у него нет прав. Права даются на основании того, что известно о коде, в основном по его месторасположению. Например, код, загружаемый из Интернета получает значительно меньше прав, чем код, установленный локально, поскольку он потенциально более опасен. Так, чтобы позволить коду работать, когда система безопасности вызвала исключение, вы должны увеличить права, предоставленные коду. Самый простой путь - переместить приложение в более доверяемое место (например, в локальную файловую систему). Но это не будет работать во всех случаях (хорошим примером являются web-приложения или интранет-приложения в корпоративной сети). Поэтому, вместо изменения месторасположения кода, вы можете изменить установки политики безопасности, чтобы дать текущему месторасположению больше прав. Это делается с помощью утилиты конфигурации .NET Framework Configuration или утилиты управления правами доступа (caspol.exe). Если вы разработчик кода, то вы можете подписать код цифровой подписью и затем модифицировать политику безопасности, чтобы дать больше прав для кода с данной подписью. Выполняя любое из этих действий, не забывайте, что код имел меньше прав, поскольку он поступил не из источника, которому полностью доверяют, поэтому, прежде чем вы переместите код на локальную машину или измените политику безопасности, вы дложны быть уверены, что данный код не выполняет никаких вредных действий.
Как администрировать политику безопасности на компьютере? В предприятии?
.NET Framework включает утилиту конфигурации .NET Framework Configuration, модуль управления для консоли MMC (mscorcfg.msc), чтобы конфигурировать определенные элементы политики безопасности. Модуль управления для консоли MMC не только поддерживает администрирование политики безопасности на локальной машине, но и создает пакеты, совместимые с System Management Server и групповой политикой безопасности (Group Policy). Утилита командной строки, CASPol.exe, может также быть использована для управления политикой безопасности на машине. Чтобы запустить любую из этих утилит, перейдите в директорию установки .NET Framework (находится в %windir%\Microsoft.Net\Framework\v1.0.2914.16\) и наберите mscorcfg.msc или caspol.exe.
Как evidence-based политика безопасности работает с политикой безопасности Windows 2000 ?
Evidence-based политика безопасности (которая авторизует код) работает вместе с политикой безопасности Windows 2000 (которая основана на индентификаторе, под которым осуществлен вход). Например, чтобы получить доступ к файлу, управляемый код должен иметь оба права - и право доступа кода к файлу и должен быть запущен под пользователем, который имеет права на файл на уровне NTFS. Управляемые библиотеки, включенные в состав .NET Framework также предоставляют классам безопасность, основанную на ролях. Это позволяет приложению работать с авторизацией Windows и пользовательскими группами.
|