45828 (665172), страница 2
Текст из файла (страница 2)
UsingExplicitDLLDlg.cpp
// код, генерируемый средой разработки … // хэндл тестируемой DLL HINSTANCE hDll = NULL; // тип указателя на функцию ViewStringGridWnd typedef HWND (__stdcall *ViewStringGridWndProcAddr) (int Count, double* Values); // хэндл окна с VCL-компонентом StringGrid HWND hGrid = NULL; // тип указателя на функцию SumFunc typedef int (__cdecl *SumFuncProcAddr) (int a, int b); // код, генерируемый средой разработки … // обработчик нажатия кнопки SumFunc void CUsingExplicitDLLDlg::OnSumFunc() { // указатель на функцию SumFunc SumFuncProcAddr ProcAddr = NULL; if( hDll != NULL ) { // получение адреса функции ProcAddr = (SumFuncProcAddr) GetProcAddress(hDll, "SumFunc"); if( ProcAddr != NULL ) { // вызов функции int result = (ProcAddr)(5, 6); // отображение результата в заголовке диалога char str[10]; this->SetWindowText(itoa(result, str ,10)); } } } // обработчик нажатия кнопки ViewStringGridWnd void CUsingExplicitDLLDlg::OnViewStringGridWnd() { // указатель на функцию ViewStringGridWnd ViewStringGridWndProcAddr ProcAddr = NULL; if( hDll != NULL ) { // получение адреса функции ProcAddr = (ViewStringGridWndProcAddr) GetProcAddress(hDll, "ViewStringGridWnd"); if( ProcAddr != NULL ) { // инициализация аргументов 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); // вызов функции hGrid = (ProcAddr)(count, Values); } } } // обработчик события окна WM_DESTROY void CUsingExplicitDLLDlg::OnDestroy() { CDialog::OnDestroy(); // закрываем окно с компонентом StringGrid, если оно было создано if( hGrid != NULL ) ::SendMessage(hGrid, WM_CLOSE, 0, 0); // выгрузка dll из памяти FreeLibrary( hDll ); } // обработчик события окна WM_CREATE int CUsingExplicitDLLDlg::OnCreate(LPCREATESTRUCT lpCreateStruct) { if (CDialog::OnCreate(lpCreateStruct) == -1) return -1; // загрузка dll в память hDll = LoadLibrary("ExplicitDll.dll"); return 0; } |
Явная загрузка dll имеет как преимущества, так и недостатки. В нашем случае большим плюсом является то, что явная загрузка избавляет от какого бы то ни было взаимодействия с исходным кодом dll, в частности нет необходимости подключать заголовочный .h-файл с объявлениями функций. Клиентское приложение компилируется и работает независимо от используемой dll, а случаи неудачной загрузки библиотеки или неудачного получения адреса функции всегда можно обыграть так, чтобы они не повлияли на дальнейшее выполнение основного приложения.
ПРИМЕЧАНИЕ Следует отметить, что использование экспортируемых unmanaged-функций из управляемого кода (managed code) в .NET осуществляется исключительно посредством явной загрузки dll. К процессу вызова функции в этом случае помимо стандартных шагов (таких как загрузка dll в память посредством LoadLibrary, получение адреса требуемой функции с помощью GetProcAddress и непосредственно вызов), добавляется также процесс маршалинга (marshaling), то есть процесс преобразования типов данных .NET в их аналоги в традиционном двоичном коде (при проталкивании аргументов в стек) и обратно (при анализе возвращаемого значения). Для указания, что метод импортируется из dll, используется атрибут DllImport, параметры которого содержат информацию, необходимую для вызова LoadLibrary и GetProcAddress. |
Таким образом, для вызова экспортируемой функции из dll, скомпилированной в BCB, необходимо выполнить следующую последовательность действийя:
Объявить экспортируемые функции либо как __cdecl, либо как __stdcall. Если используется только соглашение __stdcall, пропускаем пункт 3.
Поместить объявления функций в блок extern ”С”. Не экспортировать классы и функции-члены классов, поскольку это все равно не удастся.
Если экспортируются функции с соглашением о вызове __cdecl, то добавить к проекту .def-файл с псевдонимами для каждой такой функции.
Откомпилировать dll.
Создать клиентский (то есть использующий BCB библиотеку) VC-проект.
Скопировать созданную BCB dll в папку с клиентским VC-приложением.
Загрузить dll из клиентского приложения в память при помощи LoadLibrary.
Получить адрес требуемой функции с помощью GetProcAddress и присвоить его указателю на функцию.
Вызвать функцию с помощью указателя на нее.
По окончании использования выгрузить dll из памяти с помощью FreeLibrary.
Алгоритм с неявным связыванием для экспорта (импорта) __cdecl-функций
Как следует из названия раздела, данный способ предназначен для экспорта (а на клиентской стороне – для импорта) функций с __cdecl-соглашением о вызове. Чтобы воспользоваться неявным связыванием, прежде всего, необходимо создать объектный .lib-файл (библиотеку импорта), содержащий ссылку на dll и перечень находящихся в dll функций. Данный объектный файл можно создать по .def-файлу экспорта библиотеки с помощью утилиты lib.exe. При этом полученный .lib-файл будет в нужном нам формате COFF, поскольку компилятор VC придерживается именно этой спецификации (утилита lib.exe поставляется совместно с VC и умеет создавать библиотеки импорта только по .def-файлу). Готовый .lib-файл прилинковывается к клиентскому проекту.
При неявном связывании приложение не подозревает, что использует dll, поэтому функции, вызываемые из динамической библиотеки, как и любые другие, должны быть объявлены в тексте клиентской программы. Для объявления функций воспользуемся исходным заголовочным файлом BCB dll, но функции в нем должны быть помечены уже не как __declspec(dllexport), а как __declspec(dllimport), то есть как импортируемые извне, поскольку по отношению к клиентскому приложению эти функции являются именно импортируемыми.
Исходный текст dll на этот раз будет выглядеть следующим образом:
Листинг 3 - Компилятор Borland C++ Builder 5
ImplicitLinking_cdecl.h
#ifndef _IMPLICITDLL_ #define _IMPLICITDLL_ // если макрос-идентификатор _DLLEXPORT_ был определен ранее, // то макрос _DECLARATOR_ пометит функцию как экспортируемую, // в противном случае функция будет помечена как импортируемая. // Данная конструкция из директив препроцессора позволяет // воспользоваться заголовочным файлом библиотеки как на этапе // создания DLL, так и на этапе ее использования, а именно, при // неявном связывании. #ifdef _DLLEXPORT_ #define _DECLARATOR_ __declspec(dllexport) #else #define _DECLARATOR_ __declspec(dllimport) #endif extern "C" { int _DECLARATOR_ __cdecl SumFunc(int a, int b); HWND _DECLARATOR_ __cdecl ViewStringGridWnd(int Count, double* Values); } #endif |
ImplicitLinking_cdecl.cpp
#include #include // определение _DLLEXPORT_, дабы вместо макроса _DECLARATOR_ // в заголовочном файле было подставлено __declspec(dllexport), // и функции были объявлены как экспортируемые #define _DLLEXPORT_ #include "ImplicitLinking_cdecl.h" int __cdecl SumFunc( int a, int b ) { // тело функции такое же как в предыдущем разделе } HWND __cdecl ViewStringGridWnd( int Count, double* Values ) { // тело функции такое же как в предыдущем разделе } #pragma argsused int WINAPI DllEntryPoint(HINSTANCE hinst, unsigned long reason, void* lpReserved) { return 1; } |
Основная возникающая при этом проблема заключается в том, что, согласно таблице 1, функции с __cdecl-соглашением о вызове будут экспортироваться с символом подчеркивания, следовательно, .lib-файл, созданный по .def-файлу экспорта библиотеки, будет содержать измененные имена функций. С другой стороны, во-первых, компилятор VC будет ожидать неизмененных наименований __cdecl-функций, потому что сам VC, экспортируя функции с __cdecl-соглашением о вызове, ничего к их наименованию не добавляет, а во-вторых, заголовочный файл BCB dll, подключаемый к клиентскому приложению, содержит объявления функций с их реальными (без символа подчеркивания) именами. В результате этого, если в тексте клиентского приложения встретится хотя бы один вызов нашей функции, то VC при связывании попытается найти описание этой импортируемой функции в добавленной к проекту библиотеке импорта (.lib-файле), с тем, чтобы добавить соответствующую запись в таблицу импорта приложения. Но из-за несоответствия имен функций в заголовочном и объектном файлах линковщик, естественно, в .lib-файле ничего не найдет, о чем не замедлит выдать сообщение (например, такое - error LNK2001: unresolved external symbol __imp__SumFunc).
ПРИМЕЧАНИЕ Таблица импорта любого PE-файла содержит массив структур IMAGE_IMPORT_DESCRIPTOR. Каждая такая структура соответствует одной из dll, с которой неявно связан PE-файл. Структура IMAGE_IMPORT_DESCRIPTOR среди прочих полей содержит поле с RVA строки-наименования dll, которой она соответствует, и два поля с RVA массивов двойных слов, предназначенных для хранения информации об импортируемых функциях. При запуске приложения загрузчик PE-файлов заполняет один из этих массивов (так называемую таблицу адресов импорта) адресами импортируемых функций, загрузив перед этим dll, в которой эти функции находятся. Адрес импортируемой функции вычисляется как сумма адреса, по которому была загружена экспортирующая данную функцию dll, и смещения (RVA) самой функции относительно начала dll. |
Описанную выше проблему несоответствия заголовочного и объектного файлов можно решить двумя способами – снова воспользоваться рассмотренным в предыдущем разделе .def-файлом с псевдонимами или использовать в заголовочном файле нашей библиотеки директиву препроцессора #define.
Использование псевдонимов
Следуя этому способу, создаем и добавляем к проекту BCB dll следующий .def-файл:
ImplicitLinkingAliases.def
EXPORTS ; MSVC name = Borland name SumFunc = _SumFunc ViewStringGridWnd = _ViewStringGridWnd |
После компиляции наша dll будет экспортировать функции
ImplicitLinking_cdecl.def
libRARY IMPLICITLINKING_CDECL.DLL EXPORTS 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.DLL EXPORTS SumFunc @4 ; SumFunc ViewStringGridWnd @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…).