ПРИМЕЧАНИЕТаблица импорта любого PE-файла содержит массив структур IMAGE_IMPORT_DESCRIPTOR. Каждая такая структура соответствует одной из dll, с которой неявно связан PE-файл. Структура IMAGE_IMPORT_DESCRIPTOR среди прочих полей содержит поле с RVA строки-наименования dll, которой она соответствует, и два поля с RVA массивов двойных слов, предназначенных для хранения информации об импортируемых функциях. При запуске приложения загрузчик PE-файлов заполняет один из этих массивов (так называемую таблицу адресов импорта) адресами импортируемых функций, загрузив перед этим dll, в которой эти функции находятся. Адрес импортируемой функции вычисляется как сумма адреса, по которому была загружена экспортирующая данную функцию dll, и смещения (RVA) самой функции относительно начала dll. |
Описанную выше проблему несоответствия заголовочного и объектного файлов можно решить двумя способами – снова воспользоваться рассмотренным в предыдущем разделе .def-файлом с псевдонимами или использовать в заголовочном файле нашей библиотеки директиву препроцессора #define.
Следуя этому способу, создаем и добавляем к проекту BCB dll следующий .def-файл:
ImplicitLinkingAliases.defEXPORTS ; MSVC name = Borland name SumFunc = _SumFuncViewStringGridWnd = _ViewStringGridWnd |
После компиляции наша dll будет экспортировать функции
ImplicitLinking_cdecl.def
libRARY IMPLICITLINKING_CDECL.DLLEXPORTS SumFunc @4 ; SumFunc ViewStringGridWnd @5 ; ViewStringGridWnd _SumFunc @1 ; _SumFunc _ViewStringGridWnd @2 ; _ViewStringGridWnd___CPPdebugHook @3 ; ___CPPdebugHook |
Таким образом, в таблицу экспорта dll добавляются функции-псевдонимы, имена которых соответствуют функциям, объявленным в заголовочном файле нашей библиотеки. Для полного соответствия (хотя этого можно и не делать) удалим из ImplicitLinking_cdecl.def упоминания обо всех посторонних для приложения-клиента функциях, так как заголовочный файл содержит объявления только двух функций. В результате получим .def-файл готовый для генерации из него объектного .lib-файла:
ImplicitLinking_cdecl.def
libRARY IMPLICITLINKING_CDECL.DLLEXPORTS SumFunc @4 ; SumFuncViewStringGridWnd @5 ; ViewStringGridWnd | ||
ПРИМЕЧАНИЕВ единственной статье, которую мне удалось найти по данной теме (на сайте bcbdev.com), рекомендовалось, помимо удаления из .def-файла посторонних функций, заменить наименование секции EXPORTS на IMPORTS. Делать этого не следует по той простой причине, что утилита lib.exe (по крайней мере, поставляемая с 6-ой и 7-ой Visual Studio) секцию IMPORTS не поддерживает, поэтому игнорирует все последующие описания функций и создает пустой .lib-файл. Утилита lib.exe находится в каталоге $(VC)\Bin, но запустить ее обычно с первого раза не удается, поскольку для работы ей требуется библиотека mspdb60.dll (для lib.exe, поставляемой с Visual Studio 7 – mspdb70.dll). mspdb60.dll лежит в папке $(Microsoft Visual Studio)\Common\MSDev98\Bin, а mspdb70.dll – в папке $(Microsoft Visual Studio .NET)\Common7\IDE. |
С помощью утилиты lib.exe создадим необходимый для неявного связывания .lib-файл в формате COFF, для этого в командной строке наберем
lib.exe /def:ImplicitLinking_cdecl.def |
либо
lib.exe /def:ImplicitLinking_cdecl.def /out:ImplicitLinking_cdecl.lib |
Полученный .lib-файлдобавимкпроекту VC-клиента (Project -> Add To Project -> Files…).
Использование директивы препроцессора #define
Теперь рассмотрим способ, позволяющий добиться одинаковых названий функций в заголовочном и объектном (.lib) файлах с помощью директивы #define. Перепишем заголовочный файл нашей BCB-библиотеки следующим образом
Листинг 4 - Компилятор Borland C++ Builder 5
ImplicitLinking_cdecl.h
#ifndef _IMPLICITDLL_#define _IMPLICITDLL_#ifdef _DLLEXPORT_ #define _DECLARATOR_ __declspec(dllexport)#else #define _DECLARATOR_ __declspec(dllimport)#endifextern "C"{ // при компиляции в VC к оригинальным наименованиям // функций добавятся символы подчеркивания, таким образом // имена объявляемых функций совпадут с их именами в таблице // экспорта DLL и, следовательно, .lib-файле#ifdef _MSC_VER #define SumFunc _SumFunc #define ViewStringGridWnd _ViewStringGridWnd #endif int _DECLARATOR_ __cdecl SumFunc(int a, int b); HWND _DECLARATOR_ __cdecl ViewStringGridWnd(int Count, double* Values);}#endif |
При компиляции клиентского VC-приложения в подключенном к проекту заголовочном файле dll (ImplicitLinking_cdecl.h) к наименованию каждой функции с помощью директив #define добавляется символ подчеркивания (макрос _MSC_VER определяется компилятором VC по умолчанию). Поскольку из BCB dll __cdecl-функции экспортируются таким же образом, то есть с добавлением символа подчеркивания, то устанавливается соответствие имен экспортируемых и объявленных функций. Макросы #define распространяют свое влияние и на весь последующий код приложения, что позволяет в тексте программы при вызове импортируемой функции пользоваться ее оригинальным именем, которое при компиляции будет дополнено необходимым магическим символом подчеркивания. Таким образом, мы идем на поводу у фирмы Borland и в клиентском приложении завуалированно используем для вызова функций из нашей dll имена, измененные компилятором BCB. Именно необходимость использования измененных имен (пусть и не в открытую благодаря define-трюку), на мой взгляд, является существенным недостатком этого способа, так как, например, при желании явно (см. раздел “Алгоритм с явной загрузкой dll”) использовать dll придется оперировать измененными именами функций. Не развивая дальше эту тему, скажу, что если BCB dll создается с четким намерением использовать ее в VC-приложениях, то лучше добавлять к проекту библиотеки .def-файл с удобными для пользователей именами-псевдонимами функций.
К достоинствам данного способа (define-трюка) можно отнести его простоту и, как бы это ни противоречило сказанному в предыдущем абзаце, отсутствие необходимости добавлять к таблице экспорта dll псевдонимы функций. Несмотря на все удобства использования псевдонимов, таблица экспорта (а следовательно, и сама dll) при этом увеличивается в размерах. Да и создание .def-файла псевдонимов при большом количестве функций не добавляет приятных эмоций.
После компиляции dll с помощью impdef.exe получаем .def-файл экспорта, из которого утилитой lib.exe создаем объектный .lib-файл и добавляем его к клиентскому VC-проекту.
Листинг клиентского приложения, код которого в данном случае не зависит от способа решения проблемы несоответствия наименований функций в заголовочном и объектном файлах библиотеки, представлен ниже. Как и в предыдущем разделе, это диалоговое окно с двумя кнопками. Интересующий нас код сосредоточен в обработчиках событий нажатия кнопок диалога.
Листинг 5 - Компилятор Visual C++ 6.0
UsingImplicitLinking_cdeclDlg.cpp
// код, генерируемый средой разработки… // хэндл окна с VCL-компонентом StringGridHWND hGrid = NULL;// подключаем заголовочный файл библиотеки#include "ImplicitLinking_cdecl.h"// код, генерируемый средой разработки… void CUsingImplicitLinkng_cdeclDlg::OnSumFunc() { // вызываем функцию SumFunc из dll int res = SumFunc(5, 9);// выводим результат в заголовок диалогового окнаchar str[10]; this->SetWindowText(itoa(res, str ,10)); }void CUsingImplicitLinkng_cdeclDlg::OnViewStringGridWnd() { // инициализацияаргументов const int count = 5; double Values[count] = {2.14, 3.56, 6.8, 8, 5.6564};// закрываем ранее созданное окно, чтобы они не «плодились»if( hGrid != NULL ) ::SendMessage(hGrid, WM_CLOSE, 0, 0); // вызываемфункцию ViewStringGridWnd из dll hGrid = ViewStringGridWnd(count, Values); }void CUsingImplicitLinkng_cdeclDlg::OnDestroy() { CDialog::OnDestroy();// закрываем окно с компонентом StringGrid, если оно было созданоif( hGrid != NULL ) ::SendMessage(hGrid, WM_CLOSE, 0,0);} |
Основным преимуществом неявной загрузки dll является именно неявность использования dll со стороны клиентского приложения. Другими словами, приложение, вызывая функции, не подозревает, что они могут находиться где-то во внешнем модуле. Результатом является упрощение кода программы. К недостаткам следует отнести тот факт, что dll находится в памяти в течение всей работы программы, неявно ее использующей. Загрузка dll осуществляется при загрузке приложения – загрузчик PE-файлов, просматривая каждую запись в таблице импорта приложения, загружает соответствующую этой записи dll. Следовательно, если используемых библиотек много, загрузка основной программы может затянуться. В случае отсутствия неявно используемой dll приложение вообще не запустится.
Итоговый алгоритм с неявным связыванием для экспорта (импорта) __cdecl-функций состоит из следующей последовательности действий (см. также Демонстрационный проект):
1. Объявить экспортируемые функции как __cdecl.
2. Поместить объявления функций в блок extern ”С”, при этом не экспортировать классы и функции-члены классов.
3. В заголовочный файл для возможности его дальнейшего использования на клиентской стороне вставить:
#ifdef _DLLEXPORT_ #define _DECLARATOR_ __declspec(dllexport)#else #define _DECLARATOR_ __declspec(dllimport)#endif |
и добавить макрос _DECLARATOR_ к объявлению каждой функции, например,
int _DECLARATOR_ __cdecl SumFunc( int a, int b ); |
4. Далее либо создать и добавить к проекту .def-файл с псевдонимами для каждой функции, либо добавить в заголовочный файл библиотеки следующее:
#ifdef _MSC_VER #define FuncName1 _FuncName1 #define FuncName2 _FuncName2 #define FuncNameN _FuncNameN#endif |
Если использовался #define-трюк, то пункт 7 нужно будет пропустить.
5. Скомпилировать BCB dll.
6. С помощью impdef.exe создать .def-файл с наименованиями экспортируемых функций.