Динамические библиотеки для начинающих
(статья была опубликована в журнале «Программист»)
В наше время Windows-разработчик шагу не может ступить без динамических библиотек (Dynamic Link Library — DLL); а перед начинающими программистами, желающими разобраться в предмете, встает масса вопросов:
- как эффективно использовать чужие DLL?
- как создать свою собственную?
- какие способы загрузки DLL существуют, и чем они отличаются?
- как загружать ресурсы из DLL?
Обо всем этом (и многом другом) рассказывает настоящая глава. Материал рассчитан на пользователей Microsoft Visual C++, а поклонникам других языков и компиляторов придется разбираться с ключами компиляции приведенных примеров самостоятельно.
Создание собственной DLL
С точки зрения программиста — DLL представляет собой библиотеку функций (ресурсов), которыми может пользоваться любой процесс, загрузивший эту библиотеку. Сама загрузка, кстати, отнимает время и увеличивает расход потребляемой приложением памяти; поэтому бездумное дробление одного приложения на множество DLL ничего хорошего не принесет.
Другое дело — если какие-то функции используются несколькими приложениями. Тогда, поместив их в одну DLL, мы избавимся от дублирования кода и сократим общий объем приложений — и на диске, и в оперативной памяти.
Можно выносить в DLL и редко используемые функции отдельного приложения; например, немногие пользователи текстового редактора используют в документах формулы и диаграммы — так зачем же соответствующим функциям впустую «отъедать» память?
Чем больше функций экспортирует DLL — тем медленнее она загружается; поэтому к проектированию интерфейса (способа взаимодействия DLL с вызывающим кодом) следует отнестись повнимательнее. Хороший интерфейс интуитивно понятен программисту, немногословен и элегантен: как говорится, ни добавить, ни отнять.
Строгих рекомендаций на этот счет дать невозможно — умение приходит с опытом
Для экспортирования функции из DLL — перед ее описанием следует указать ключевое слово __declspec(dllexport), как показано в следующем примере:
// myfirstdll.c#include // Ключевое слово __declspec(dllexport)// делает функцию экспортируемой
__declspec(dllexport) void Demo(char *str)
{ // Выводим на экран переданную функции Demo строку printf(str);}
Листинг 10 Демонстрация экспорта функции из DLL
Для компиляции этого примера в режиме командной строки можно запустить компилятор Microsoft Visual Studio: «cl.exe myfirstdll.c /LD«. Ключ «/LD» указывает линкеру, что требуется получить именно DLL.
Для сборки DLL из интегрированной оболочки Microsoft Visual Studio — при создании нового проекта нужно выбрать пункт «Win32 Dynamics Link Library«, затем «An Empty DLL project«; потом перейти к закладке «File View» окна «Workspace» — и, выбрав правой клавишей мыши папку «Source Files«, добавить в проект новый файл («Add Files to Folder«). Компиляция осуществляется как обычно («Build» ( «Build»).
Если все прошло успешно — в текущей директории (или в директории ReleaseDebug при компиляции из оболочки) появится новый файл — «MyFirstDLL.dll». Давайте заглянем в него через «микроскоп» — утилиту dumpbin, входящую в штатную поставку SDK и Microsoft Visual Studio: «dumpbin /EXPORTS MyFirstDLL.dll«. Ответ программы в несколько сокращенно виде должен выглядеть так:
Section contains the following exports for myfirst.dll 0 characteristics 0.00 version 1 ordinal base 1 number of functions 1 number of names
1 0 00001000 Demo
Получилось! Созданная нами DLL действительно экспортирует функцию «Demo» — остается только разобраться, как ее вызывать
Вызов функций из DLL
Существует два способа загрузки DLL: с явной и неявной компоновкой.
Принеявной компоновке функции загружаемой DLL добавляются в секцию импорта вызывающего файла.
При запуске такого файла загрузчик операционной системы анализирует секцию импорта и подключает все указанные библиотеки.
Ввиду своей простоты этот способ пользуется большой популярностью; но простота — простотой, а неявной компоновке присущи определенные недостатки и ограничения:- все подключенные DLL загружаются всегда, даже если в течение всего сеанса работы программа ни разу не обратится ни к одной из них;
- если хотя бы одна из требуемых DLL отсутствует (или DLL не экспортирует хотя бы одной требуемой функции) — загрузка исполняемого файла прерывается сообщением «Dynamic link library could not be found» (или что-то в этом роде) — даже если отсутствие этой DLL некритично для исполнения программы. Например, текстовой редактор мог бы вполне работать и в минимальной комплектации — без модуля печати, вывода таблиц, графиков, формул и прочих второстепенных компонентов, но если эти DLL загружаются неявной компоновкой — хочешь не хочешь, придется «тянуть» их за собой.
- поиск DLL происходит в следующем порядке: в каталоге, содержащем вызывающий файл; в текущем каталоге процесса; в системном каталоге %Windows%System%; в основном каталоге %Windows%; в каталогах, указанных в переменной PATH. Задать другой путь поиска невозможно (вернее — возможно, но для этого потребуется вносить изменения в системный реестр, и эти изменения окажут влияние на все процессы, исполняющиеся в системе — что не есть хорошо).
Явная компоновка устраняет все эти недостатки — ценой некоторого усложнения кода. Программисту самому придется позаботиться о загрузке DLL и подключении экспортируемых функций (не забывая при этом о контроле над ошибками, иначе в один прекрасный момент дело кончится зависанием системы).
Зато явная компоновка позволяет подгружать DLL по мере необходимости и дает программисту возможность самостоятельно обрабатывать ситуации с отсутствием DLL. Можно пойти и дальше — не задавать имя DLL в программе явно, а сканировать такой-то каталог на предмет наличия динамических библиотек и подключать все найденные к приложению.
Именно так работает механизм поддержки plug-in’ов в популярном файл-менеджере FAR (да и не только в нем).
Таким образом, неявной компоновкой целесообразно пользоваться лишь для подключения загружаемых в каждом сеансе, жизненно необходимых для работы приложения динамических библиотек; во всех остальных случаях — предпочтительнее явная компоновка.
Загрузка DLL с неявной компоновкой
Чтобы вызвать функцию из DLL, ее необходимо объявить в вызывающем коде — либо как external (т. е. как обычную внешнюю функцию), либо предварить ключевым словом __declspec(dllimport).
Первый способ более популярен, но второй все же предпочтительнее — в этом случае компилятор, поняв, что функция вызывается именно из DLL, сможет соответствующим образом оптимизировать код.
Например, функция «Demo» из созданной нами библиотеки — «MyFirstDll» вызывается так:
// ImplictDll.c // Объявляем внешнюю функцию Demo
__declspec(dllimport) void Demo(char *str);
main() { // Вызываем функцию Demo из DLL Demo(«Hello, World!»); }
Листинг 11 Демонстрация вызова функции из DLL неявной компоновкой
Из командной строки данный пример компилируется так: «cl.exe ImplictDll.c myfirstdll.lib«, где «myfirstdll.lib» — имя библиотеки, автоматически сформированной компоновщиком при создании нашей DLL.
Разумеется, «чужие» DLL не всегда поставляются вместе с сопутствующими библиотеками, но их можно легко изготовить самостоятельно! На этот случай предусмотрена специальная утилита implib, поставляемая вместе с компилятором, и вызываемая так: «implib.exe Имя_файла _создаваемой_библиотеки Имя_DLL«.
В нашем случае — не будь у нас файла «MyFirstDLL.lib«, его пришлось бы получить так: «implib.exe MyFirstDLL.lib MyFirstDLL.dll«. Со всеми стандартными DLL, входящими в состав Windows, эту операцию проделывать не нужно, т.к. необходимые библиотеки распространяются вместе с самим компилятором.
Для подключения библиотеки в интегрированной среде Microsoft Visual Studio — в меню «Project» выберите пункт «Project Settings«, в открывшемся диалоговом окне перейдите к закладке «Link» и допишите имя библиотеки в конец строки «Object/Library Modules«, отделив ее от остальных символом пробела.
Заглянем внутрь: как это происходит? Запустим «dumpbin /IMPORTS ImplictDll.exe» ипосмотрим, что нам сообщит программа:
File Type: EXECUTABLE IMAGE Section contains the following imports:
myfirstdll.dll
404090 Import Address Table 4044C8 Import Name Table 0 time date stamp 0 Index of first forwarder reference
0 Demo
KERNEL32.dll 404000 Import Address Table 404438 Import Name Table 0 time date stamp 0 Index of first forwarder reference 19B HeapCreate 2BF VirtualFree CA GetCommandLineA 174 GetVersion 7D ExitProcess 29E TerminateProcess F7 GetCurrentProcess
Вот она — «Myfirstdll.dll» (в тексте выделена жирным шрифтом), и вот функция «Demo«, а кроме нее — обнаруживает свое присутствие библиотека KERNEL32.
DLL – она необходима RTL-коду (Run Time Library — библиотека времени исполнения), насильно помещенному компилятором в наше приложение.
RTL-код обеспечивает работу с динамической памятью (heap), считывает аргументы командной строки, проверяет версию Windows и многое-многое другое! Отсюда и появляются в таблице импорта функции HeapCreate, GetCommandLine, GetVersion и т.д. Так что — не удивляйтесь, увидев «левый» импорт в своем приложении!
Проследить, как именно происходит загрузка DLL, можно с помощью отладчика. Общепризнанный лидер — это, конечно, SoftIce от NuMega, но для наших экспериментов вполне сойдет и штатный отладчик Microsoft Visual Studio. Откомпилировав нашу вызывающую программу, нажмем для пошагового прогона приложения
Оппаньки! Не успело еще выполниться ни строчки кода, как в окне «output» отладчика появились следующие строки, свидетельствующие о загрузке внешних DLL: NTDLL.DLL, MyFirstDll.dll и Kernel32.dll.
Так и должно быть — при неявной компоновке динамические библиотеки подключаются сразу же при загрузке файла, задолго до выполнения функции main!
Loaded 'C:WINNTSystem32 dll.dll', no matching symbolic information found.Loaded 'F:ARTICLEPRGDLL.filesmyfirstdll.
dll', no matching symbolic information found.Loaded 'C:WINNTsystem32kernel32.dll', no matching symbolic information found.Явную загрузку динамических библиотек осуществляет функция HINSTANCE LoadLibrary(LPCTSTR lpLibFileName) или ее расширенный аналог HINSTANCE LoadLibraryEx(LPCTSTR lpLibFileName, HANDLE hFile, DWORD dwFlags).
Обе они экспортируются из KERNEL32.DLL, следовательно, каждое приложение требует неявной компоновки по крайней мере этой библиотеки. В случае успешной загрузки DLL возвращается линейный адрес библиотеки в памяти.
Передав его функции FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName) — мы получим указатель на функцию lpProcName, экспортируемую данной DLL. При возникновении ошибки обе функции возвращают NULL.
После завершения работы с динамической библиотекой ее следует освободить вызовом функции BOOL FreeLibrary
Ошибка работы с функциями из внешних динамически загружаемых библиотек
Структуру библиотечных ресурсов операционной системы Windows можно представить следующим образом:
- Сначала, на самом нижнем уровне, идут «прерывания» самой системы, «ноги» которых растут ещё с самых первых версий Windows, более того, всё от той давно почившей «старушки» MS-DOS (да, на этом уровне за 30 лет почти ничего в Windows и не изменилось). Доступ к этой библиотеке проще всего через ассемблер по команде INT (interrupt), отсюда и «прерывания», хотя на самом деле к подпрограммам обработки прерываний этот уровень имеет мало отношения.
- Поднимаемся на шаг выше и мы попадаем в WinSDK – набор разработчика программного обеспечения от Windows. Это своеобразный аналог всё тех же «прерываний», но уже с учётом специфики организации Windows – разработчик получает доступ к этой библиотеке через привычный ему код используемого языка высокого уровня.
Единственная трудность – терминология описания WinSDK отличается от терминологии описания самого прикладного языка программирования, его функций (взять те же Builder или Delphi). Благо, все функции WinSDK подробно описаны в самих средах разработчиков (собственно, никакого отношения к ним не имеющих, настолько велика их популярность).
Функции WinSDK «собираются» в файлах динамически подключаемых (дословно — загружаемых) библиотек – dynamic load library (dll). Пользователю такой библиотеки нет необходимости знать адрес функции внутри – ему достаточно знать имя функции и иметь уверенность, что функция в библиотеке есть. Точки входов в функции определяются в описании самой библиотеки.
- Ещё поднимаемся выше и мы в библиотеках программ-надстроек над Windows. Одной из самых известных надстроек является графический пакет DirectX. Здесь такая же организация – всё те же dll-файлы.
Вот с одной из библиотек DirectX и связана ошибка d3d11.dll – в ней хранятся функции отображения объёмных графических моделей.
Небольшие уточнения
Тут нужно понимать, что когда на экране появляется простое сообщение «Ошибка d3d11.dll», это не совсем верное сообщение, оно не точно отражает суть проблемы. Иногда встречается сообщение об ошибке «could not create d3d11 device» — как исправить, такую ошибку сам текст мало чем может помочь.
Точнее это сообщение должно было бы выглядеть как – «Ошибка входа в функцию xyz, в библиотеке d3d11.dll» или проще – «Функция xyz в библиотеке d3d11.dll не определена».
Другое дело, что разработчики приложений часто не удосуживаются уточнениями и дают именно простой, «неопределённый» вариант характера возникшей ошибки. Но сути это не меняет. Основные причины сбоя – или библиотека d3d11.dll отсутствует, как вариант – d3d11.dll не был найден, или не содержит необходимой функции, или повреждена настолько, что функция оказывается недоступной.
Исправление ошибки
Исправление ошибки доступа к функции в d3d11.dll заключается в перестановке самой библиотеки. DLL – исполняемый код, войти в такой файл и его отредактировать не получится. Более того, файл относится к 11-ой версии пакета DirectX.
Таким образом, первый способ устранить ошибку – переставить пакет DirectX 11. Этот способ прост и давно проверен, главное только найти рабочую версию самой надстройки, сделать это лучше всего на официальном сайте Microsoft.
Установка DirectX в Windows
Другой способ – переставить только сам файл d3d11.dll. Это можно сделать с помощью специализированной утилиты dll-files.com, которая бесплатна и доступна на одноимённом сайте.
Итак, устанавливаем и запускаем dll-files.
Поиск DLL-файла в программе DLL-files.com
В строке поиска вводим «d3d11.dll». После того, как библиотека будет найдена, нажимаем «Установить».Установка DLL-файла в программе DLL-files.com
Если вы привыкли всё делать самостоятельно, то проблему можно решить и вручную – это уже третий способ. Для его применения скачиваем файл d3d11.dll и размещаем его в нужной системной папке. Тут нужно быть внимательным, так как эта папка в разных версиях Windows разная (в примерах предполагается, что система установлена на диск «C:», если это не так, то и диск должен быть соответствующий):
- в версиях Windows XP, Vista, 7, 8, 8.1 и 10 — C:WindowsSystem32;
- в Windows 95, 98 и Me — C:WindowsSystem;
- в Windows NT и 2000 — C:WINNTSystem32.
Примем во внимание – если стоит ещё и версия системы для 64-разрядного процессора, то конечная папка для нашего файла в любом случае – «SysWOW64».
Перепись файла в папку ещё не обеспечивает работу с ним. Теперь его нужно зарегистрировать в системном реестре Windows. Для этого выполняем через окно команд (Пуск/Главное меню/Выполнить) команду: regsvr32 d3d11.dll.
Заключение
Решение подавляющего большинства проблем, связанных с внешними динамически загружаемыми библиотеками операционной системы Windows, часто связано просто с перестановкой самой библиотеки, или самостоятельно, или через общий пакет (в нашем примере – DirectX 11).
Есть возможность установить в системе dll-файл и вручную, простым копированием его в нужную папку. Но, во-первых, нужно знать эту папку. А, во-вторых, такой файл необходимо будет зарегистрировать в системном реестре Windows. Тут не обойтись уже без служебных утилит работы с реестром самой системы.
С++ & dll & lib статическая и динамическая линковка библиотек
Немного поговорим о том, что вам интересно, но вы стесняетесь спросить. Виды распространения программных компонентов, повторное использование кода и актуальных проблемах.
Этапы сборки проекта
Программа на C++ обычно состоит из объявлений (типов, функций) и определений (реализаций тел функций, методов, выделение памяти под глобальные и статические объекты и т.п.). Объявления, как правило, выносятся в заголовочные файлы (т.н. «хэдеры») с расширением (*.h), а определения оказываются в файлах исходного кода с расширением (*.cpp).
Заголовочные файлы играют вспомогательную функцию и в команды не компилируются сами непосредственно (их содержимое копируются в файлы исходного кода с помощью команды #include «header_file_name.h»). Файлы исходного кода содержат алгоритмы и компилируются непосредственно в команды процессора в виде объектных файлов (*.
obj), по одному на каждый файл исходного кода. Чтобы собрать объектные файлы в один двоичный файл (непосредственно исполняемый *.exe, или библиотеки *.dll (*.so для Linux), *.lib) необходимо произвести линковку. Этим занимается программа Linker (компоновщик, линкер), ориентируясь по хэдерам.Программирование ценно модульностью.
То есть написав однажды код для работы, например, с аппаратом теории графов, мы можем его использовать во многих других проектах. Для этого мы можем распространять как полностью открытый исходный код, так и бинарные (двоичные) библиотеки, которые уже скомпилированы, но можно прилинковать к своему проекту.
Плюсы:
- Достаточно распространять одну версию для многих платформ и аппаратного обеспечения (соблюдая требования переносимости).
- Код компилируется конкретным компилятором пользователя под конкретную платформу, возможности оптимизации максимальны, программа может выполняться быстрее.
- Клиент может ознакомиться с алгоритмов, внести поправки при необходимости.
Недостатки:
- Раз алгоритмы открыты, ваши наработки (по крайней мере, идейные, если опасаться ограничений свободного ПО (LGPL, GPL и др.)) можно обнаружить в чужих проектах. Если вы хотели продавать свои модули — забудьте.
- Исходный код имеет размер больше, чем его скомпилированное двоичное представление.
Бинарные библиотеки lib и dll
Библиотеки могут также распространяться в виде скомпилированных двоичных файлов, которые могут быть прилинкованы к программе клиента. Линковка может быть статическая и динамическая. Статическая представляет собой собранние *.obj-файлов библиотеки в *.
lib, который мы можем, указав линкеру, прицепить к нашей программе в момент компиляции. Содержимоей библиотеки, как всегда, описывается в хэдерах, которые распространяются вместе с *.lib . На выходе мы получим одинокий исполняемый файл вашей программы (*.exe).
Динамическая линковка выполняется средствами платформы (операционной системы) в процессе работы программы. Все так же у нас в руках *.lib и *.h файлы, однако, теперь к ним добавляется *.dll (*.so для Linux). *.lib-файл теперь содержит только вызовы к *.
dll, где лежат непосредственно алгоритмы и которые вызываются уже на ходу, а не компилируются. Потому теперь у нас *.exe + *.dll . Несколько программ могут использовать один *.dll одновременно, тем самым не занимая оперативную память одинаковыми кусками и сами программы меньше размером.
Так, например, работают многие драйверы и графические библиотеки (DirectX и OpenGL).
Однако, сейчас это не такая актуальная проблема, тянут недостатки — несовместимости версий, отсутствие нужных библиотек, ад зависимостей для установки приложений (работая в Linux с графическим окружением Gnome (основанной на библиотеке GTK+) если скачать малюсенький текстовый редактор Kate для ГО KDE (основанной на Qt), то придется тянуть этот-самый Qt на десятки мегобайт). Потому, сейчас рекомендуют не увлекаться динамической линковкой и стараться связывать программы статически.
Статическая компоновка
Пусть у нас есть простой проект с одним классом, который мы хотим передать клиенту в двоичном виде:
//——————// Header.h//——————class SomeType { int i;public: SomeType(); SomeType& incr(int value = 1); int getI();
};
И его реализация://—————————// Implementation.
cpp//—————————#include «Header.h»SomeType::SomeType() : i(0){}SomeType& SomeType::incr(int value/* = 1*/) { i += value; return *this;}int SomeType::getI() { return i;
}
Ничего интересного, тип инкапсулирует переменную, которую мы можем увеличивать и получать ее значение.Попробуем слинковать статически с другим проектом в Microft Visual Studio 2010://——————————// main.cpp//——————————#include #include #include «Header.
h» // Обращаемся к хэдеру библиотекиusing std::cout;using std::endl;int main() { SomeType st; st.incr().incr(3).incr(); cout имя_проекта Properties…), там на Configuration Properties -> C/C++ в Additional Include Directories добавим папку с хэдером библиотеки (можно также добавить хэдер в раздел Header Files в Solution Explorer`е к остальным исходникам).
Теперь нам нужно сказать линкеру, чтобы он нашел и прилинковал *.lib к нашему проекту. Там же в Configuration Properties -> Linker -> General в Additional Library Directories указать папку с *.lib .
И в соседнем пункте дерева настроек линкера Input найти самый верхний пункт Additional Dependencies, куда добавить имя_библиотеки.lib . Все готово! Клиент не знает, что делают ваш Implementation.cpp, ибо у него на руках только имя_библиотеки.lib и Header.h .
Динамическая компоновка
Операции аналогичны статической линковке за исключением того, что теперь придется таскать с собой *.dll . Для компиляции динамической библиотеки необходимо в библиотечном проекте снова изменить тип проекта на Dynamic library (.
dll), скомпилировать и положить dll туда, где появится исполняемый файл клиентской программы или в %windir%System32 (операции же с хэдерами и lib те же, что и при статической линковке). Нужно изменить кое-что в коде.
Нам необходимо указать, какие функции/классы будут «торчать наружу» в dll (при ее компиляции) с помощью спецификатора _declspec(dllexport) и какие будут ориентированы на вызов из dll от клиента с помощью похожего _declspec(dllimport). Ставятся перед именем в объявлении функции/класса, т.е.
при компиляции dll и при компиляции клиента (exe) меняется только хэдер. Для этого пишется простой макрос, который определяется перед импортом (#import) хэдера в dll. Клиентский код не меняется (только настройки проекта). Новый вид такой:
//——————// Header.h//——————#ifdef DLL_EXPORT# define DLL_API _declspec(dllexport)#else# define DLL_API _declspec(dllimport)#endifclass DLL_API SomeType { int i;public: SomeType(); SomeType& incr(int value = 1); int getI();
};
И его реализация://—————————// Implementation.
cpp//—————————/*Определяем макрос, чтобы при компиляции dllспецификатор стал dllexport, на клиенте жемакрос не определяется, потому будет dllimport*/#define DLL_EXPORT#include «Header.
h»SomeType::SomeType() : i(0){}SomeType& SomeType::incr(int value/* = 1*/) { i+=value; return *this;}int SomeType::getI() { return i;
}
Загрузка функции из dll динамически, средствами Win32 API:
//—————// header.h//—————#ifdef BUILD_DLL #define DLL_EXPORT __declspec(dllexport)#else #define DLL_EXPORT __declspec(dllimport)#endifint DLL_EXPORT Foo(int a, int b);//——————// implementation.cpp//—————— #include «header.h» int DLL_EXPORT Foo(int a, int b) { return a * b;}
Клиентский код:
//——————// client.cpp//——————#include «header.h»#include #include using std::cout;int main() { // Определяем соответствующий указатель на ф-ю typedef int (*Multiplies)(int x, int y); Multiplies mm; // Средствами WinAPI загружаем библиотеку, извлекаем // адрес функции в указатель HINSTANCE h = 0; h = LoadLibrary(«libfile.dll»); mm = reinterpret_cast(GetProcAddress(h, «Foo»)); // … // PROFIT!!1 cout