45828 (665172), страница 3
Текст из файла (страница 3)
Использование директивы препроцессора #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) #endif extern "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-компонентом StringGrid HWND 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-файл с наименованиями экспортируемых функций.
7. Если в пункте 4 воспользовались псевдонимами, удалить из .def-файла экспорта неиспользуемые наименования функций, оставив только псевдонимы.
8. Создать клиентский VC-проект.
9. Из .def-файла экспорта библиотеки при помощи утилиты lib.exe создать объектный .lib-файл формата COFF и добавить его к клиентскому VC-приложению.
10. Скопировать BCB dll и ее заголовочный файл в папку с клиентским VC-проектом.
11. В клиентском приложении подключить заголовочный файл dll.
12. Вызвать в теле программы необходимые функции, не задумываясь над тем, что они расположены во внешней dll.
Алгоритм с неявным связыванием для экспорта (импорта) __stdcall-функций
Как уже упоминалось выше, утилита lib.exe может создавать библиотеку импорта только из .def-файла экспорта, при чем lib.exe при этом никак не взаимодействует с самой dll. Однако .def-файл не содержит никакой информации, касаемой соглашений о вызове, которых придерживаются экспортируемые функции. Следовательно, и lib.exe, работая исключительно с .def-файлом, не сможет уловить, что имеет дело с __stdcall-функциями, и, как результат, не сможет в .lib-файле отобразить функции согласно Microsoft-соглашению о наименовании для __stdcall-функций. Таким образом, учитывая из предыдущего раздела, что для __cdecl-функций lib.exe генерирует вполне работоспособный .lib-файл, приходим к следующему выводу: утилита lib.exe не способна генерировать библиотеки импорта для dll, экспортирующих __stdcall-функции. Людям, пожелавшим или вынужденным (а после прочтения этого раздела думаю только вынужденным) использовать BCB dll с __stdcall-функциями в VC, этот раздел посвящается.
Исходный код BCB dll остался таким же, как в предыдущем разделе (см. Листинг 3), только ключевое слово __cdecl везде необходимо заменить ключевым словом __stdcall.
Известно, что при создании VC dll вместе с ней среда генерирует .lib-файл (библиотеку импорта), который представлен, естественно, в нужном нам формате COFF, и в котором корректно будут отображаться __stdcall-функции. Поэтому создадим (File -> New… -> Win32 Dynamic-Link Library -> OK -> An empty DLL project -> Finish) ложную (dummy) VC dll, которая будет экспортировать тот же набор функций, что и BCB dll. Реализация функций в ложной dll абсолютно не важна, важны исключительно их наименования. Помимо одинаковых наименований экспортируемых функций у ложной и исходной библиотек должны совпадать имена, поскольку .lib-файлы содержат наименования dll. Можно воспользоваться исходными текстами BCBdll, скопировав .h- и .cpp-файлы в директорию к ложной dll, затем добавив их к проекту (Project -> Add To Project -> Files…) и удалив тела всех функций. Если функция возвращает значение, то оставляем оператор return и возвращаем в соответствии с типом все, что угодно (можно 0, NULL и т.д.). Поскольку тела функций будут пустыми, большую часть директив #include с подключаемыми заголовочными файлами также можно удалить. В итоге получим согласно нашему примеру следующий код ложной dll:
Листинг 6 - Компилятор Visual C++ 6.0
ImplicitLinking_stdcallDummy.h
#ifdef _DLLEXPORT_ #define _DECLARATOR_ __declspec(dllexport) #else #define _DECLARATOR_ __declspec(dllimport) #endif extern "C" { int _DECLARATOR_ __stdcall SumFunc(int a, int b); HWND _DECLARATOR_ __stdcall ViewStringGridWnd(int Count, double* Values); } |
ImplicitLinking_stdcallDummy.cpp
#define _DLLEXPORT_ #include #include "ImplicitLinking_stdcallDummy.h" int __stdcall SumFunc(int a, int b) { return 0; } HWND __stdcall ViewStringGridWnd(int Count, double* Values) { return NULL; } |
Согласно таблице 1, VC экспортирует __stdcall-функции, добавляя к их наименованию информацию о списке аргументов и символ подчеркивания. Следовательно, в объектном .lib-файле будут имена, отличные от оригинальных имен функций, объявленных в заголовочном файле, и тем более отличные от наименований функций, экспортируемых из BCB dll, так как __stdcall-функции компилятор BCB экспортирует без изменений. Избавляться от этого несоответствия будем снова посредством .def-файла. Для нашего примера он будет следующим:
DummyDef.def
libRARY ImplicitLinking_stdcall.dll EXPORTS SumFunc ViewStringGridWnd |
Строка с именем библиотеки (LIBRARY) в .def-файле не обязательна, но если она есть, то имя, указанное в ней, в точности должно совпадать с именами ложной и исходной dll. Добавляем .def-файл к VC-проекту, перекомпилируем и получаем ложную dll и необходимую нам библиотеку импорта, содержащую корректное описание экспортируемых __stdcall-функций. .lib-файл, доставшийся в наследство от ложной dll, должен добавляться (прилинковываться) к любому VC-проекту, который собирается использовать нашу исходную BCB dll.
Пример VC-приложения, импортирующего __stdcall-функции, такой же, как и в предыдущем разделе (см. Листинг 5). Не забудьте в примере подключить (#include) нужный заголовочный файл BCB dll и добавить к проекту нужную библиотеку импорта.
Алгоритм с неявным связыванием для экспорта (импорта) __stdcall-функций (см. также Демонстрационный проект, ImplicitLinkingDll_stdcall.zip):