В этой статье будут рассмотрены методы
автоматизации кодирования импорта функций из динамически подключаемых DLL.
Решение этой задачи позволит значительно сократить трудоёмкость (а значит, и
время, и количество внесённых ошибок) написания похожего кода в разных проектах
и/или для разных DLL.
Материал проиллюстрирован исходными текстами на
Borland C++ Builder 6.0; однако все опубликованные идеи справедливы (и применимы
с минимальной адаптацией) для любого языка/среды разработки.
Введение и библиография
В журнале "Программист" (№9 за 2002 г.) была
опубликована статья Вячеслава Ермолаева "Использование template-классов при
импортировании функций из DLL".
Уважаемый автор предлагает для решения этой задачи использовать механизм
шаблонов (templates) языка С++. Это огромный шаг вперёд по сравнению с рутинным
кодированием, но на мой взгляд, не идеальное решение, которому присущи некоторые
недостатки:
Всё разнообразие возможных функций ограничивается богатством заранее
описан-ных шаблонов; и хотя в статье автор отодвинул планку очень далеко - 13
параметров функций (а много ли Вы припомните функций, не укладывающихся в эти
рамки ?), всё же сам факт несвободы как-то стесняет
Если Вы всё же где-то откопаете функцию с большим количеством параметров,
Вам придётся расширить набор шаблонов, т.е. изменить библиотеку
классов, что является крайне нетехнологичным решением -
Вы же не изменяете исходники VCL!
При кодировании пользовательского приложения приходится использовать
до-вольно-таки громоздкое описание импортируемых функций в обёртках
template-классов, что не способствует мнемонической лёгкости чтения и
понимания текста
Механизм шаблонов языка C++ позволяет уменьшить объём исходного, но никак
не объектного, полученного после компиляции кода, который раздувается тем
сильнее, чем более разнообразны параметры в импортируемых из DLL функциях.
Кроме того, в рамках предложенного метода остаётся нерешённой проблема
автоматизации довольно трудоёмкой рутинной операции - определения
полных идентифика-торов функций в DLL (строковых параметров для функции
GetProcAddress):
Особых проблем не наблюдается, если все функции в DLL скомпилированы как
"extern "C" - в этом случае линкер просто добавляет символ подчёрки-вания
перед именем функции
Если же DLL собрана с функциями в стиле C++, всё совсем не так
одно-значно: идентификаторы могут получиться до полуметра длиной , и
выковыривание их из DLL - лишняя ручная работа; можно, конечно, зная
прави-ла работы линкера, синтезировать их - но и это явно предмет для
автоматизации, к тому же подозреваю, что у разных средств разработки
(BCB, Visual C++) раз-ные правила, по которым работает линкер
Решение
Как прекрасна была бы жизнь, если б можно было все насущные
нужды разработчика удовлетворить только средствами языка разработки!
Увы, нет в мире совершенства, как говорил Лис, и поэтому фирмы-разработчики
средств разработки генерируют всё новые, всё более мощные среды разработки
(IDE), а также развивают сами языки программирования - взять те же Delphi, BCB,
C# : сравните языковые средства с Pascal и C++. Вспомните также, сколько
дополнительных (встроенных в IDE и отдельных) инструментальных средств входит в
поставку BCB и Delphi.
Borland Software Corporation, отдав дань уважения OWL, задвинула её подальше
и стала развивать Delphi и BCB на платформе VCL. Совершенно не вижу, почему бы
благородным донам не поступить так же J.
Суть моего решения состоит в
интеграции средства языка - компоненты - и
дополнительного инструментального средства собственной
разработки - DllWizard. Интерфейс-оболочку к DLL обеспечивает компонента TAskDll
(исходный код - в архиве AskDll.zip). В её методах инкапсулированы:
динамическая загрузка DLL (функция LoadLibrary) в методе LoadDll
обработка исключительных ситуаций: при возникновении любых проблем с
за-грузкой DLL формируется сообщение на языке локализации Windows и
генерируется ис-ключение (Exception) для обработки в вызывающем приложении
выгрузка DLL и освобождение памяти (функция FreeLibrary), выполняемые
авто-матически при прекращении существования компоненты (например, при
закрытии формы, на которой расположена компонента)
Загрузка DLL и
инициализация импортируемых функций осуществляется вызовом одного лишь метода
компоненты - LoadDll. Параметр метода - указатель на функцию:
Всё, что нам нужно - это написать подобный код. Именно эта часть работы
наиболее тру-доёмка, и когда мы сможем выполнить её легко, быстро и безошибочно,
это и будет красивым венцом нашей технологии.
Задача решается с помощью
DllWizard в 3 прохода:
Автоматическое формирование описаний функций с их параметрами и
возвращае-мыми значениями.
Автоматическое формирование идентификаторов функций (строковых параметров
для функции GetProcAddress)
Генерация исходных текстов
Рассмотрим пример работы с Example.DLL,
экспортирующей 2 функции:
int __cdecl Func1(int i1, int i2, int i3);
char * __cdecl Func2(char * s1, char * s2
Итак, начинаем:
Запускаем DllWizard и создаём список всех функций, которые мы хотим
импорти-ровать из DLL. Если DLL собственной разработки, достаточно просто
указать путь к её исходнику и нажать кнопку "Найти": список сформируется
автоматически (см.рис. 1)
Указываем путь к DLL и нажимаем кнопку "Найти" (см.рис. 2)
Нажимаем кнопку "Сгенерировать" - в каталогах, указанных на закладке
"На-стройки" будут сформированы файлы
рис. 1
рис. 2
Имя DLL-ки является префиксом у всех сгенерированных файлов и у функции в
модуле CPP. Исходный текст DLL и тестового приложения находится в архиве
DllTest.zip
Подкаталог DLL содержит исходный текст библиотеки:
UnitExample.cpp Подкаталог EXE содержит исходный текст тестового приложения:
UnitDllTest.cpp и в подкаталоге DllWizard - сгенерированные файлы:
Заголовочные файлы:
Example_Descript.h является служебным и содержит описание функций
Example_Declare.h является служебным и содержит объявления указателей на
функции
Example_Extern.h следует включить в тот исходный модуль проекта
прило-жения, из которого вызываются функции, импортируемые из DLL.
Example_Load.cpp содержит функцию загрузки Example_Load
Резюме
Подводим итоги. Ниже описан порядок разработки пользовательского
приложения, им-портирующего функции из динамически подключаемой библиотеки
функций:
С помощью DllWizard генерируем включаемые модули
В проект включаем сгенерированный модуль Example_Load.cpp
"Бросаем" на главную форму компоненту TAskDll и в её свойстве DllName
указы-ваем имя DLL-ки (см.рис. 3)
рис. 3
В модуль главной формы и во все модули, в которых предполагается
использовать импортируемые из DLL функции, включаем заголовочный файл
Example_Extern.h
Пишем пользовательский код и компилируем проект. Всё! Скриншот работы
тес-тового приложения приведён на рис. 4
рис. 4
Преимущества технологии
Разнообразие импортируемых функций не ограничено ничем
Не изменяются коды библиотеки (компоненты)
Не происходит разбухания объектного кода, т.к. не используются шаблоны -
все функции конкретно и явно описаны в заголовочных файлах
Полностью автоматизированный процесс генерации кода, включая
опреде-ление идентификаторов функций (параметров для GetProcAddress)
Мнемоника кода не ухудшается: имена функций остаются неизменными
Минимальный объём ручного кодирования - всего 2 строки:
Включение заголовочного файла
Вызов метода LoadDll
Технология применима не только для BCB, но и для, например, Visual C++,
а также - с небольшой адаптацией - для любого языка/среды разработки;
Например, в Visual C++:
сгенерированный код можно использовать без изменений (только
за-комментировав включение vcl.h)
вместо компоненты TAskDll следует создать класс.
Многие разработчики делают компоненты-обёртки для функций DLL - их
применение намного удобнее. Для этих целей как нельзя лучше подходит данная
технология:
Создаётся компонента, производная от TAskDll
Сгенерированный модуль (Example_Load.cpp) включается в проект пакета
В конструкторе компоненты свойству DllName присваивается имя DLL
В методе Loaded компоненты вызывается метод LoadDll. Всё!
Заключение
Я успешно использую эту технологию в своей работе. DllWizard
- по сути тривиальная утилита - была написана, отлажена и локализована
(английский и русский интерфейсы) за 3 часа. В настоящий момент она не является
инструментом общего пользования; ей присущи некоторые ограничения, так как я
писал "под себя", а не "для дяди". Тем не менее, мне не жалко поделиться тем,
что есть. Хочу ещё раз подчеркнуть, что ограничения свойственны конкретной
реализации, а не идее:
Генерируется код только для С++
Нет полноценного лексического анализа исходного текста DLL:
предполагается, что функции описаны в одну строку вида:
__declspec(dllexport) char* Func2(char *s1, char *s2) , где
__declspec(dllexport) - без пробелов
тип возвращаемого значения - без пробелов
после имени функции в скобках следует описание параметров
Не поддерживается анализ перегруженных функций в одной DLL: например,
void Func()
long Func(long lp)
Тем не менее, трудно отнести это к недостаткам, т.к. это крайне редкая
ситуация
P.S. Немножко саморекламы: компонента TAskDll является составной частью
пакета ASK Tools, см. соответствующую ссылку на http://goldenask.narod.ru/
Автоматизация кодирования импорта функций из DLL: с помощью DllWizard обеспечивается быстрый и надежный процесс генерации кода для импортируемых функций из динамически подключаемых DLL.
Комментарии и вопросы
Получайте свежие новости и обновления по Object Pascal, Delphi и Lazarus прямо в свой смартфон. Подпишитесь на наш Telegram-канал delphi_kansoftware и будьте в курсе последних тенденций в разработке под Linux, Windows, Android и iOS
Материалы статей собраны из открытых источников, владелец сайта не претендует на авторство. Там где авторство установить не удалось, материал подаётся без имени автора. В случае если Вы считаете, что Ваши права нарушены, пожалуйста, свяжитесь с владельцем сайта.