Создание Dynamic-Link Library в RAD Studio

Создание Dynamic-Link Library в RAD Studio

Знакомство с Dynamic-Link Library

Что такое DLL

Для начала нужно разобраться, что же такое DLL.
DLL — это сокращение фразы Dynamic-Link Library — динамически подключаемая библиотека.
Первоначальная идея введения библиотек заключалась в более рациональном использовании ресурсов ПК, таких как ОЗУ и HDD. Предполагалось, что различные приложения будут использовать одну библиотеку, тем самым ликвидируя проблему сохранения одинаковых участков кода в различных местах и многократную загрузку одинаковых участков в ОЗУ.

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

По сути, DLL — это относительно независимый участок кода, имеющий свою оболочку.
Оболочка DLL имеет секции импорта/экспорта.

Секция экспорта перечисляет те идентификаторы объектов (классы, функции, переменные), доступ к которым предоставляет данная DLL. В большинстве случаев именно эта секция вызывает особый интерес у разработчиков. Тем не менее, DLL может вовсе не содержать секцию экспорта. Кроме функций, к которым можно получить доступ извне (exported), существуют также внутренние функции (internal), которые спрятаны от «посторонних глаз» и могут использоваться лишь кодом самой DLL.

Секция импорта предназначена для связи данной DLL с другими.
Большинство библиотек импортируют функции из системных DLL (kernel32.dlluser32.dll и др.). Иногда в стандартный список нужно добавить другие библиотеки, например ws2_32.dll для работы с Socket‘ами.

Использование DLL

DLL не может выполняться сама по себе, поэтому требует хост-процесс (главный процесс, в котором предполагается использование DLL).
Перед тем, как библиотеку можно будет использовать, её необходимо загрузить в область памяти хост-процесса. В exe-файле компилятором будет сгенерирована инструкция вызова заданной функции (call).
Т.к. DLL расположена в адресном пространстве процесса, то вызов функции из библиотеки будет иметь вид:

При выполнении команды call, процессор передает управление на код с адреса 010199, и выполняет его до команды ret, а затем возвращает управление команде следующей за call. Код который вызывается командой call называется процедурой.

Файл, являющийся динамически загружаемой библиотекой не всегда носит расширение *.dll.
Есть также: *.cpl (библиотеки, используемые апплетом панели управления) и *.ocx.

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

DLL может являться также обычным хранилищем ресурсов, о создании которого я рассказывал в теме https://www.cyberforum.ru/cpp-… 27736.html.

Необходимость внедрения DLL. Нужно ли это?

Настало время ответить на вопрос: Когда же нужно использовать DLL?
Если вы разрабатываете небольшой проект или тестовое приложение, то внедрение DLL будет для вас пустой тратой времени и сил. Тратой времени: не только времени на создание библиотеки, но ещё и времени, необходимого для загрузки DLL в память хост-процесса. До того, как вы будете использовать объекты из DLL, вам рано или поздно прийдётся выгрузить содержимое в память. А это требует некоторого времени.
Однако, если вашу DLL будет использовать более, чем одна копия приложения (или несколько различных приложений) — наступит явный выигрыш.

Для эксперимента запустите Microsoft Office. Первый запуск долгий. Это обусловлено тем, что все необходимые модули загрузаются в оперативную память. Теперь полностью закройте Office и снова откройте его! Окно появится почти мгновенно. Это обусловлено тем, что модули хранились в ОЗУ (система не тронет их, пока ей не понадобится память для других целей).

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

Но не старайтесь придать вашему проекту чрезвычайной «уникальности» за счёт помещения каждой функции в отдельную библиотеку! Это можно делать, только зачем тратить время для загрузки каждого модуля?

Практическая часть. Создание Dynamic-Link Library в RAD Studio.

Создаём первую DLL

Пришло время постепенно перейти от теории к практике.
Открываем IDE (для написании данной статьи я использовал RAD Studio 2010).
Переходим к File -> New -> Other -> Dynamic-Link Library.

Source Type — язык, на котором ведётся разработка.
Use VCL — использование библиотеки визуальных компонентов.
Multi Threaded — опция, указывающая на то, будет ли использоваться многопоточность в данной DLL (VCL уже подразумевает в себе многопоточность).
VC++ Style DLL — опция, указывающая на то, будет ли DLL совместима с компиляторами Microsoft.

Если в совместимости нет нужды и опция не выбрана, то DLL будет иметь точку входу с таким прототипом:

Для обеспечения совместимости точка входа изменяется на:

Оставим диалог без изменений и нажмём «ОК«.
Перед нами появится шаблон минимальной DLL. Сохраним его.
Как вы помните, сама по себе DLL работать не может, ей нужен клиент. Поэтому, для удобства сразу создадим новый проект VCL Forms Application.
Для этого переходим в Project Manager, вызываем контекстное меню у нашей Project Group и переходим к Add New Project -> VCL Forms Application.
Для удобста я назвал проекты TestDLL и TestVCL соответственно (и сохранил их в одном каталоге — это избавит меня от копирования DLL или указания абсолютного пути):
Название: 2.png
Просмотров: 13308

Размер: 12.3 Кб
Без изменений запускаем TestVCL, сохраняем и переключаемся к проекту TestDLL (дабл-клик на проекте в Project Manager).

Переходим к Run -> Parameters и в поле Host Application указываем путь к нашему проекту TestVCL.

К шаблону DLL добавляем функцию, которая будет вычислять сумму и выводить результат на экран:

В проекте TestDLL добавим также заголовочный файл (TestDLL.h) с таким содержанием:

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

Неявная загрузка.

При неявной загрузке DLL загружается (проецируется на адресное пространство вызывающего процесса) при его создании. Если при загрузке возникает ошибка — процесс останавливается и разрушается.

Для выполнения неявной загрузки приложению требуются:
— Заголовочный файл (*.h) с прототипами функций, описаниями классов и типов, которые используются в приложении.
— Библиотечный файл (*.lib), в котором описывается список экспортируемых из DLL функций (переменных), и их смещения, необходимые для правильной настройки вызовов функций.

В проекте TestVCL подключим наш заголовочный файл:

Также, не забываем сделать Project -> Add To Project и добавить в проект TestDLL.lib
Далее, объявим прототип:

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

Запускаем и смотрим на результат. Думаю, «3 + 2 = 5» всех устраивает. 

Явная загрузка.

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

При этом, основными функциями являются:
LoadLibrary[Ex] — позволяют загрузить DLL в адресное пространство хост-процесса.
FreeLibrary — функция, используемая для явной выгрузки DLL.
GetProcAddress — функция, позволяющая получить виртуальный адрес экспортируемой из DLL функции(или переменной) для ее последующего вызова.

Общая методика выглядит так:
1. Загрузить DLL с помощью LoadLibrary.
2. Получить указатели на необходимые объекты с помощью GetProcAddress.
3. Выгрузить DLL после завершения всех действий.

Теперь возникает вопрос, как же проверить теорию на практике?
Всё, что нужно, это добавить TestDLL.lib к проекту (также, как и при неявной загрузке).
А дальше, для проверки снова пишем в конструкторе формы:

И на экране снова красуется победная надпись «3 + 2 = 5» 

Остался один неосвещенный вопрос. Почему же название функции «ShowSum» мы ищем в библиотеке с нижним подчёркиванием?

Виновато во всём декорирование имён.

Декорирование (или искажение, mangling) имен — это специфическое явление, присущее компиляторам языка C++, которое необходимо учитывать при разработке DLL на этом языке. Оно заключается в том, что компилятор С++ к имени функции всегда добавляет сокращенный список формальных параметров и тип возвращаемого значения.

Прототип функции Test(int); мог быть преобразован компилятором, например в ?Test@@YAKH@Z.
Естественно, такое декорирование нам вообще не по душе. Избавиться от него можно объявляя все экспортируемые функции с модификатором extern «C» — тогда компилятор не будет искажать имя функции.

Однако, как мы видим, нижние подчёркивание всё же добавилось.
Это один из нюансов среды C++ Builder. Однако, можно отучить его добавлять нижнее подчёркивание таким образом:
Project -> Options -> C++ Compiler -> Output -> Generate underscores on symbol names — перевести в состояние false.

Отложенная загрузка.

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

Использование отложенной загрузки DLL в C++ Builder мало отличается от неявной загрузки.
В проект добавляется заголовочный (*.h) файл с описаниями и библиотечный файл (*.lib).
Далее, переходим в Project -> Options -> C++ Linker -> Advanced -> Delay Load DLLs и вписываем название нашей библиотеки (TestDLL.dll).

Когда библиотека теряет свою необходимость её нужно явно выгрузить с помощью __FUnloadDelayLoadedDLL. В качестве параметра передаём имя DLL (с расширением + параметр регистрозависим).
Если вы используете многопоточные приложения — убедитесь, что все потоки завершили работу с DLL.

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

Заключение.

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

Подведя итоги можно выявить плюсы и минусы описанных методов:

Явная загрузка:
+ контроль и управление процессом жизни DLL.
 управлением DLL занимается программист посредством WinAPI.

Неявная загрузка:
+ все заботы берет на себя компилятор и сборщик.
 ресурсы заняты всё время жизни приложения.

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *