Работа с библиотеками динамической компоновки (DLL). Вызов функций из DLL Вызов функции из dll

Жаропонижающие средства для детей назначаются педиатром. Но бывают ситуации неотложной помощи при лихорадке, когда ребенку нужно дать лекарство немедленно. Тогда родители берут на себя ответственность и применяют жаропонижающие препараты. Что разрешено давать детям грудного возраста? Чем можно сбить температуру у детей постарше? Какие лекарства самые безопасные?

Использование DLL (динамически подключаемая библиотека) широко распространено в программировании Windows. DLL на самом деле часть кода исполняемого файла с расширением DLL . Любая программа может вызывать DLL .

Преимущество DLL заключается в следующем:

  • Повторное использование кода.
  • Совместное использование кода приложениями.
  • Разделение кода.
  • Улучшение потребления ресурсов в Windows.

Создание DLL

В меню File выберите пункт New -> Other . В диалоговом окне на вкладке New выберите DLL Wizard . Автоматически будет создан модуль – пустой шаблон будущей DLL .

Синтаксис DLL

Для того, чтобы построить DLL , выберите Project -> Build Имя_проекта .

Видимость функций и процедур

Функции и процедуры могут быть локальными и экспортируемыми из DLL .

Локальные

Локальные функции и процедуры могут быть использованы внутри DLL . Они видны только внутри библиотеки и ни одна программа не может их вызывать извне.

Экспортируемые

Экспортируемые функции и процедуры могут быть использованы за пределами DLL . Другие программы могут вызывать такие функции и процедуры.

Исходный код выше использует экспортируемую функцию. Имя функции следует за зарезервированным словом Exports .

Загрузка DLL

В Delphi есть два вида загрузки DLL :

Статическая загрузка

При запуске приложения загружается автоматически. Она остается в памяти на протяжении выполнения программы. Очень просто использовать. Просто добавьте слово external после объявления функции или процедуры.

Function SummaTotal(factor: integer): integer; register; external "Example.dll";

Если DLL не будет найден, программа будет продолжать работать.

загружается в память по мере необходимости. Ее реализация более сложная, потому что Вы сами должны загружать и выгружать ее из памяти. Память используется более экономно, поэтому приложение работает быстрее. Программист сам должен следить, чтобы все работало правильно. Для этого нужно:

  • Объявить тип описываемой функции или процедуры.
  • Загрузить библиотеку в память.
  • Получить адрес функции или процедуры в памяти.
  • Вызвать функцию или процедуру.
  • Выгрузить библиотеку из памяти.

Объявление типа, описывающего функцию

type TSumaTotal = function(factor: integer): integer;

Загрузка библиотеки

dll_instance:= LoadLibrary("Example_dll.dll");

Получаем указатель на функцию

@SummaTotal:= GetProcAddress(dll_instance, "SummaTotal");

Вызов функции

Label1.Caption:= IntToStr(SummaTotal(5));

Выгрузка библиотеки из памяти

FreeLibrary(dll_instance);

Динамический вызов DLL требует больше работы, но легче управлять ресурсами в памяти. Если Вы должны использовать DLL в пределах программы, тогда предпочтительнее статическая загрузка. Не забывайте использовать блок try…except и try…finally , чтобы избежать ошибок во время выполнения программы.

Экспорт строк

Созданная DLL с использованием Delphi, может быть использована и в программах, написанных на других языках программирования. По этой причине мы не можем использовать любой тип данных. Типы, которые существуют в Delphi могут отсутствовать в других языках. Желательно использовать собственных типы данных из Linux или Windows. Наша DLL может быть использована другим программистом, который использует другой язык программирования.

Можно использовать строки и динамические массивы в DLL , написанной в Delphi, но для этого нужно подключить модуль ShareMem в раздел uses в DLL и программе, которая будет ее использовать. Кроме того, это объявление должно быть первым в разделе uses каждого файла проекта.

Типов string , как мы знаем, С, С++ и других языках не существует, поэтому желательно использовать вместо них PChar .

Пример DLL

library Example_dll; uses SysUtils, Classes; var { Объявляем переменные } k1: integer; k2: integer; { Объявляем функцию } function SummaTotal(factor: integer): integer; register; begin factor:= factor * k1 + factor; factor:= factor * k2 + factor; result:= factor; end; { Экспортируем функцию для дальнейшего использования программой } exports SummaTotal; { Инициализация переменных } begin k1:= 2; k2:= 5; end.

Пример вызова функции из DLL

unit Unit1; interface uses Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls; type TForm1 = class(TForm) Button1: TButton; procedure Button1Click(Sender: TObject); private { Private declarations } public { Public declarations } end; type TSummaTotal = function(factor: integer): integer; var Form1: TForm1; implementation {$R *.dfm} procedure TForm1.Button1Click(Sender: TObject); var dll_instance: Thandle; SummaTotal: TSummaTotal; begin dll_instance:= LoadLibrary("Example_dll.dll"); @SummaTotal:= GetProcAddress(dll_instance, "SummaTotal"); Label1.Caption:= IntToStr(SummaTotal(5)); FreeLibrary(dll_instance); end; end.

Существует три способа загрузки DLL:

а) неявная;

в) отложенная.

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

Библиотечный включаемый файл с описаниями используемых объектов из DLL (прототипы функций, объявления классов и типов). Этот файл используется компилятором.

LIB-файл со списком импортируемых идентификаторов. Этот файл нужно добавить в настройки проекта (в список библиотек, ис­пользуемых компоновщиком).

Компиляция проекта осуществляется обычным образом. Используя объектные модули и LIB файл, а также учитывая ссылки на импор­тируемые идентификаторы, компоновщик (линкер, редактор связей) формирует загрузочный ЕХЕ модуль. В этом модуле компоновщик помещает также раздел импорта, где перечисляются имена всех необ­ходимых DLL-модулей. Для каждой DLL в разделе импорта указыва­ется, на какие имена функций и переменных встречаются ссылки в ко­де исполняемого файла. Эти сведения будет использовать загрузчик операционной системы.

Что же происходит на этапе выполнения клиентского приложения? После запуска ЕХЕ-модуля загрузчик операционной системы выпол­няет следующие операции:

1. Создает виртуальное адресное пространство для нового про­цесса и проецирует на него исполняемый модуль;

2. Анализирует раздел импорта, определяя все необходимые DLL-модули и тоже проецируя их на адресное пространство процесса.

DLL может импортировать функции и переменные из другой DLL. А значит, у нее может быть собственный раздел импорта, для которого необходимо повторить те же действия. В результате на инициализа­цию процесса может уйти довольно длительное время.

После отображения ЕХЕ-модуля и всех DLL-модулей на адресное пространство процесса его первичный поток готов к выполнению, и приложение начинает работу.

Недостатками неявной загрузки являются обязательная загрузка библиотеки, даже если обращение к ее функциям не будет происхо­дить, и, соответственно, обязательное требование к наличию библио­теки при компоновке.

Явная загрузка DLL устраняет отмеченные выше недостатки за счет некоторого усложнения кода. Программисту приходится самому заботиться о загрузке DLL и подключении экспортируемых функций. Зато явная загрузка позволяет подгружать DLL по мере необходимо­сти и дает возможность программе обрабатывать ситуации, возни­кающие при отсутствии DLL.

В случае явной загрузки процесс работы с DLL происходит в три этапа:

1. Загрузка DLL с помощью функции LoadLibrary (или ее рас­ширенного аналога LoadLibraryEx). В случае успешной загрузки функция возвращает дескриптор hLib типа HMODULE , что позволяет в дальнейшем обращаться к этой DLL.

2. Вызовы функции GetProcAddress для получения указателей на требуемые функции или другие объекты. В качестве первого пара­метра функция GetProcAddress получает дескриптор hLib, в качестве второго параметра -адрес строки с именем импортируемой функции. Далее полученный указатель используется клиентом. Например, если это указатель на функцию, то осуществляется вызов нужной функции.

3. Когда загруженная динамическая библиотека больше не нуж­на, рекомендуется ее освободить, вызвав функцию FreeLibrary. Осво­бождение библиотеки не означает, что операционная система немед­ленно удалит ее из памяти. Задержка выгрузки предусмотрена на тот случай, когда эта же DLL через некоторое время вновь понадобится какому-то процессу. Но если возникнут проблемы с оперативной па­мятью, Windows в первую очередь удаляет из памяти освобожденные библиотеки.

Рассмотрим отложенную загрузку DLL. DLL отложенной загрузки (delay-load DLL) это неявно связываемая DLL, которая не загружает­ся до тех пор, пока код не обратится к какому-нибудь экспортируемо­му из нее идентификатору. Такие DLL могут быть полезны в следую­щих ситуациях:

Если приложение использует несколько DLL, его инициализа­ция может занимать длительное время, требуемое загрузчику для про­ецирования всех DLL на адресное пространство процесса. DLL отло­женной загрузки позволяют решить эту проблему, распределяя загруз­ку DLL в ходе выполнения приложения.

Если приложение предназначено для работы в различных вер­сиях ОС, то часть функций может появиться лишь в поздних версиях ОС и не использоваться в текущей версии. Но если программа не вы­зывает конкретной функции, то DLL ей не нужна, и она может спо­койно продолжать работу. При обращении же к несуществующей

функции можно предусмотреть выдачу пользователю соответствую­щего предупреждения.

Для реализации метода отложенной загрузки требуется дополни­тельно в список библиотек компоновщика добавить не только нужную библиотеку импорта MyLib.lib, но еще и системную библиотеку им­порта delayimp.lib. Кроме этого, требуется добавить в опциях компо­новщика флаг / delayload:MyLib.dll.

Перечисленные настройки заставляют компоновщик выполнить следующие операции:

Внедрить в ЕХЕ-модуль специальную функцию
delayLoadHelper;

Удалить MyLib.dll из раздела импорта исполняемого модуля, чтобы загрузчик операционной системы не пытался выполнить неяв­ную загрузку этой библиотеки на этапе загрузки приложения;

Добавить в ЕХЕ-файл новый раздел отложенного импорта со списком функций, импортируемых из MyLib.dll;

Преобразовать вызовы функций из DLL к вызовам
delayLoadhelper.

На этапе выполнения приложения вызов функции из DLL реализу­ется обращением к delayLoadHelper. Эта функция, используя инфор­мацию из раздела отложенного импорта, вызывает сначала LoadLibrary, а затем GetprocAddress. Получив адрес DLL-функции, delayLoadHelper делает так, чтобы в дальнейшем эта DLL-функция вызывалась напрямую. Отметим, что каждая функция в DLL настраи­вается индивидуально при первом ее вызове.

Существует два способа загрузки DLL: с явной и неявной компоновкой.

При неявной компоновке функции загружаемой DLL добавляются в секцию импорта вызывающего файла. При запуске такого файла загрузчик операционной системы анализирует секцию импорта и подключает все указанные библиотеки. Ввиду своей простоты этот способ пользуется большой популярностью; но простота - простотой, а неявной компоновке присущи определенные недостатки и ограничения:

все подключенные DLL загружаются всегда, даже если в течение всего сеанса работы программа ни разу не обратится ни к одной из них;

если хотя бы одна из требуемых DLL отсутствует (или DLL не экспортирует хотя бы одной требуемой функции) - загрузка исполняемого файла прерывается сообщением "Dynamic link library could not be found" (или что-то в этом роде) - даже если отсутствие этой DLL некритично для исполнения программы. Например, текстовой редактор мог бы вполне работать и в минимальной комплектации - без модуля печати, вывода таблиц, графиков, формул и прочих второстепенных компонентов, но если эти DLL загружаются неявной компоновкой - хочешь не хочешь, придется "тянуть" их за собой.

поиск DLL происходит в следующем порядке: в каталоге, содержащем вызывающий файл; в текущем каталоге процесса; в системном каталоге %Windows%System%; в основном каталоге %Windows%; в каталогах, указанных в переменной PATH. Задать другой путь поиска невозможно (вернее - возможно, но для этого потребуется вносить изменения в системный реестр, и эти изменения окажут влияние на все процессы, исполняющиеся в системе - что не есть хорошо).

Явная компоновка устраняет все эти недостатки - ценой некоторого усложнения кода. Программисту самому придется позаботиться о загрузке DLL и подключении экспортируемых функций (не забывая при этом о контроле над ошибками, иначе в один прекрасный момент дело кончится зависанием системы). Зато явная компоновка позволяет подгружать DLL по мере необходимости и дает программисту возможность самостоятельно обрабатывать ситуации с отсутствием DLL. Можно пойти и дальше - не задавать имя DLL в программе явно, а сканировать такой-то каталог на предмет наличия динамических библиотек и подключать все найденные к приложению. Именно так работает механизм поддержки plug-in’ов в популярном файл-менеджере FAR (да и не только в нем).

Как вы, наверное, знаете, в динамически подключаемых библиотеках (DLL) используются соглашения языка C при объявлении экспортируемых объектов, в то время как в C++ применяется несколько иная система генерации имен при компиляции, так что нельзя просто экспортировать функции - методы класса С++ и затем использовать их в коде приложения-клиента (здесь и далее под клиентом подразумевается приложение, использующее DLL). Однако это можно сделать при помощи интерфейсов, доступных и DLL, и клиентскому приложению. Этот метод очень мощный и в то же время элегантный, т.к. клиент видит только абстрактный интерфейс, а фактический класс, который реализует все функции может быть любым. Microsoft"овская технология COM (Component Object Model) построена на подобной идее (плюс дополнительная функциональность, конечно). В этой статье будет рассказано, как использовать "классовый" подход с применением интерфеса, похожего на COM, при раннем (на этапе компиляции) и позднем (во время работы программы) связывании.

Если вы хоть раз работали с DLL, то уже знаете, что DLL имееет особенную функцию DllMain(). Эта функция подобна WinMain, или main() в том смысле, что это своего рода точка входа в DLL. Операционная система автоматически вызывает эту функцию в случаае, если DLL загружается и выгружается. Обычно эту функцию ни для чего другого не используют.

Существует два метода подключения DLL к проекту - это раннее (на этапе компиляции программы) и позднее (во время выполнения программы) связывание. Методы различаются способом загрузки DLL и способом вызова функций, реализованных и экспортированных из DLL.

Раннее связывание (во время компиляции программы)

При таком методе связывания операционная система автоматически загружает DLL во время запуска программы. Однако требуется, чтобы в разрабатываемый проект был включен.lib файл (библиотечный файл), соответствующий данной DLL. Этот файл определяет все экспортируемые объекты DLL. Объявления могут содержать обычные функции C или классы. Все, что нужно клиенту - использовать этот.lib файл и включить заголовочный файл DLL - и ОС автоматически загрузит эту DLL. Как видно, этот метод выглядит очень простым в использовании, т.к. все прозрачно. Однако вы должны были заметить, что код клиента нуждается в перекомпиляции всякий раз, когда изменяется код DLL и, соответственно, генерируется новый.lib файл. Удобно ли это для вашего приложения - решать вам. DLL может объявить функции, которые она хочет экспортировать, двумя методами. Стандартный метод - использование.def файлов. Такой.def файл - это просто листинг функций, экспортируемых из DLL.

//============================================================ // .def файл LIBRARY myfirstdll.dll DESCRIPTION "My first DLL" EXPORTS MyFunction //============================================================ // заголовок DLL, который будет включен в код клиента bool MyFunction(int parms); //============================================================ // реализация функции в DLL bool MyFunction(int parms) { // делаем все, что нужно............ }

Я думаю, можно не говорить, что в данном примере экспортируется только одна функция MyFunction. Второй метод объявления экспортируемых объектов специфичен, но намного мощнее: вы можете экспортировать не только функции, но также классы и переменные. Давайте посмотрим на на фрагмент кода, сгенерированный при создании DLL AppWizard"ом VisualC++. Комментариев, включенных в листинг, вполне хватает, чтобы понять, как все это работает.

//============================================================ // Заголовок DLL, который должен быть включен в код клиента /* Следующий блок ifdef - стандартный метод создания макроса, который далает экспорт из DLL проще. Все файлы этой DLL компилируются с определенным ключом MYFIRSTDLL_EXPORTS. Этот ключ не определяется для любого из проектов, использующих эту DLL. Таким образом, любой проект, в который включен это файл, видит функции MYFIRSTDLL_API как импортируемые из DLL, тогда как сама DLL эти же функции видит как экспортируемые. */ #ifdef MYFIRSTDLL_EXPORTS #define MYFIRSTDLL_API __declspec(dllexport) #else #define MYFIRSTDLL_API __declspec(dllimport) #endif // Класс экспортируется из test2.dll class MYFIRSTDLL_API CMyFirstDll { public: CMyFirstDll(void); // TODO: здесь можно добавить свои методы. }; extern MYFIRSTDLL_API int nMyFirstDll; MYFIRSTDLL_API int fnMyFunction(void);

Во время компиляции DLL определен ключ MYFIRSTDLL_EXPORTS, поэтому перед объявлениями экспортируемых объектов подставляется ключевое слово __declspec(dllexport). А когда компилируется код клиента, этот ключ неопределен и перед объектами появляется префикс __declspec(dllimport), так что клиент знает, какие объекты импортируются из DLL.

В обоих случаях все, что нужно сделать клиенту - добавить файл myfirstdll.lib в проект и включить заголовочный файл, который объявляет импортируемые из DLL объекты, а затем использовать эти объекты (функции, классы и переменные) точно так же, как будто они были определены и реализованы локально в проекте. А теперь давайте разберем другой метод использования DLL, который чаще бывает удобнее и мощнее.

Позднее связывание (во время работы программы)

Когда используется позднее связывание, DLL загружается не автоматически, при запуске программы, а напрямую в коде, там, где это нужно. Не нужно использовать никакие.lib файлы, так что клиентское приложение не требует перекомпиляции при изменении DLL. Такое связывание обладает мощными возможностями именно потому, что ВЫ решаете, когда и какую DLL загрузить. Например, вы пишете игру, в которой используется DirectX и OpenGL. Вы можете просто включить весь необходимый код в исполняемый файл, но тогда разобраться в чем-нибудь будет просто невозможно. Или можно поместить код DirectX в одну DLL, а код OpenGL - в другую и статически подключить их к проекту. Но теперь весь код взаимнозависим, так что если вы написали новую DLL, содержащую код DirectX, то перекомпилировать придется и исполняемый файл. Единственным удобством будет то, что вам не нужно заботиться о загрузке (хотя неизвестно, удобство ли это, если вы загружаете обе DLL, занимая память, а в действительности нужна лишь одна из них). И наконец, на мой взгляд, лучшая идея состоит в том, чтобы позволить исполняемому файлу решить, какую DLL загрузить при запуске. Например, если программа определила, что система не поддерживает акселерацию OpenGL, то лучше загрузить DLL с кодом DirectX, иначе загрузить OpenGL. Поэтому позднее связывание экономит память и уменьшает зависимость между DLL и исполняемым файлом. Однако в этом случае накладывается ограничение на экспортируемые объекты - экспортироваться могут лишь C-style функции. Классы и переменные не могут быть загружены, если программа использует позднее связывание. Давайте посмотрим, как обойти это ограничение с помощью интерфейсов.

DLL, спроектированная для позднего связывания обычно использует.def файл для определения тех объектов, которые она хочет экспортировать. Если вы не хотите использовать.def файл, можно просто использовать префикс __declspec(dllexport) перед экспортируемыми функциями. Оба метода делают одно и то же. Клиент загружает DLL, передавая имя файла DLL в функцию Win32 LoadLibrary().Эта функция возвращает хэндл HINSTANCE, который используется для работы с DLL и который необходим для выгрузки DLL из памяти, когда она становится не нужна. После загрузки DLL клиент может получить указатель на любую функцию при помощи функции GetProcAddress(), используя в качестве параметра имя требуемой функции.

//============================================================ // .def файл LIBRARY myfirstdll.dll DESCRIPTION "My first DLL" EXPORTS MyFunction //============================================================ /* Реализация функции в DLL */ bool MyFunction(int parms) { //делаем что-нибудь............ } //============================================================ //Код клиента /* Объявление функции в действительности необходимо только для того, чтобы опредедлить параметры. Объявления функций обычно содержаться в заголовочном файле, поставляемом вместе с DLL. Ключевое слово extern C в объявлении функции сообщает компилятору, что нужно использовать соглашения об именовании переменных языка C. */ extern "C" bool MyFunction(int parms); typedef bool (*MYFUNCTION)(int parms); MYFUNCTION pfnMyFunc=0; //указатель на MyFunction HINSTANCE hMyDll = ::LoadLibrary("myfirstdll.dll"); if(hMyDll != NULL) { //Определяем адрес функции pfnMyFunc= (MYFUNCTION)::GetProcAddress(hMyDll, "MyFunction"); //Если неудачно - выгружаем DLL if(pfnMyFunc== 0) { ::FreeLibrary(hMyDll); return; } //Вызываем функцию bool result = pfnMyFunc(parms); //Выгружаем DLL, если она больше нам не нужна::FreeLibrary(hMyDll); }

Как вы видите, код довольно прямолинеен. А теперь давайте посмотрим, как может быть реализована работа с "классами". Как было указано ранее, если используется позднее связывание, нет прямого способа импортировать из DLL классы, так что нам нужно реализовать "функциональность" класса с помощью интерфейса, содержащего все открытые (public) функции, исключая конструктор и деструктор. Интерфейс будет обычной C/C++ структурой, содержащей только виртуальные абстрактные функции-члены. Фактический класс в DLL будет наследоваться от этой структуры и будет реализовывать все функции, определенные в интерфейсе. Теперь, чтобы получить доступ к этому классу из приложения - клиента, все, что нужно сделать - это экспортировать C-style функции, соответствующие экземпляру класса и связать их с определенным нами интерфейсом для того, чтобы клиент мог их использовать. Для реализации такого метода нужна еще две функции, одна из которых создаст интерфейс, а вторая удалит интерфейс после того, как с ним закончили работать. Пример реализации этой идеи приведен ниже.

//============================================================ // .def файл LIBRARY myinterface.dll DESCRIPTION "реализует интерфейс I_MyInterface EXPORTS GetMyInterface FreeMyInterface //============================================================ // Заголовочный фал, используемый в Dll и клиенте, // который объявляет инетрфейс // I_MyInterface.h struct I_MyInterface { virtual bool Init(int parms)=0; virtual bool Release()=0; virtual void DoStuff() =0; }; /* Объявления экспортируемых функций Dll и определения типов указателей на функции для простой загрузки и работы с функциями. Обратите внимание на префикс extern "C", который сообщает компилятору о том, что используются С-style функции */ extern "C" { HRESULT GetMyInterface(I_MyInterface ** pInterface); typedef HRESULT (*GETINTERFACE)(I_MyInterface ** pInterface); HRESULT FreeMyInterface(I_MyInterface ** pInterface); typedef HRESULT (*FREEINTERFACE)(I_MyInterface ** pInterface); } //============================================================ //Реализация интерфейса в Dll // MyInterface.h class CMyClass: public I_MyInterface { public: bool Init(int parms); bool Release(); void DoStuff(); CMyClass(); ~CMyClass(); //любые другие члены класса............ private: //любые члены класса............ }; //============================================================ // Экспортируемые функции, которые создают и уничтожают интерфейс // Dllmain.h HRESULT GetMyInterface(I_MyInterface ** pInterface) { if(!*pInterface) { *pInterface= new CMyClass; return S_OK; } return E_FAIL; } HRESULT FreeMyInterface(I_MyInterface ** pInterface) { if(!*pInterface) return E_FAIL; delete *pInterface; *pInterface= 0; return S_OK; } //============================================================ // Код клиента //Объявления интерфейса и вызов функций GETINTERFACE pfnInterface=0;//указатель на функцию GetMyInterface I_MyInterface * pInterface =0;//указатель на структуру MyInterface HINSTANCE hMyDll = ::LoadLibrary("myinterface.dll"); if(hMyDll != NULL) { //Определяем адрес функции pfnInterface= (GETINTERFACE)::GetProcAddress(hMyDll, "GetMyInterface"); //Выгружаем DLL, если предыдущая операция окончилась неудачей if(pfnInterface == 0) { ::FreeLibrary(hMyDll); return; } //Вызываем функцию HRESULT hr = pfnInterface(&pInterface); //Выгружаем, если неудачно if(FAILED(hr)) { ::FreeLibrary(hMyDll); return; } //Интерфейс загружен, можно вызывать функции pInterface->Init(1); pInterface->DoStuff(); pInterface->Release(); //Освобождаем интерфейс FREEINTERFACE pfnFree = (FREEINTERFACE)::GetProcAddress(hMyDll,"FreeMyInterface"); if(pfnFree != 0) pfnFree(&hMyDll); //Выгружаем DLL::FreeLibrary(hMyDll); }

Этой информации вполне достаточно, чтобы вы почувствовали все удобство использования интерфейсов. Удачного программирования!

С самого рождения (или чуть позже) операционная система Windows использовала библиотеки динамической компоновки DLL (Dynamic Link Library), в которых содержались реализации наиболее часто применяемых функций. Наследники Windows - NT и Windows 95, а также OS/2 - тоже зависят от библиотек DLL в плане обеспечения значительной части их функциональных возможностей.

Рассмотрим ряд аспектов создания и использования библиотек DLL:

  • как статически подключать библиотеки DLL;
  • как динамически загружать библиотеки DLL;
  • как создавать библиотеки DLL;
  • как создавать расширения МFC библиотек DLL.

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

Практически невозможно создать приложение Windows, в котором не использовались бы библиотеки DLL. В DLL содержатся все функции Win32 API и несчетное количество других функций операционных систем Win32.

Вообще говоря, DLL - это просто наборы функций, собранные в библиотеки. Однако, в отличие от своих статических родственников (файлов. lib), библиотеки DLL не присоединены непосредственно к выполняемым файлам с помощью редактора связей. В выполняемый файл занесена только информация об их местонахождении. В момент выполнения программы загружается вся библиотека целиком. Благодаря этому разные процессы могут пользоваться совместно одними и теми же библиотеками, находящимися в памяти. Такой подход позволяет сократить объем памяти, необходимый для нескольких приложений, использующих много общих библиотек, а также контролировать размеры ЕХЕ-файлов.

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

Чаще всего проект подключается к DLL статически, или неявно, на этапе компоновки. Загрузкой DLL при выполнении программы управляет операционная система. Однако, DLL можно загрузить и явно, или динамически, в ходе работы приложения.

Библиотеки импортирования

При статическом подключении DLL имя.lib-файла определяется среди прочих параметров редактора связей в командной строке или на вкладке "Link" диалогового окна "Project Settings" среды Developer Studio. Однако.lib-файл, используемый при неявном подключении DLL, - это не обычная статическая библиотека. Такие.lib-файлы называются библиотеками импортирования (import libraries). В них содержится не сам код библиотеки, а только ссылки на все функции, экспортируемые из файла DLL, в котором все и хранится. В результате библиотеки импортирования, как правило, имеют меньший размер, чем DLL-файлы. К способам их создания вернемся позднее. А сейчас рассмотрим другие вопросы, касающиеся неявного подключения динамических библиотек.

Согласование интерфейсов

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

Если бы мир был совершенен, то программистам не пришлось бы беспокоиться о согласовании интерфейсов функций при подключении библиотек - все они были бы одинаковыми. Однако мир далек от совершенства, и многие большие программы написаны с помощью различных библиотек без C++.

По умолчанию в Visual C++ интерфейсы функций согласуются по правилам C++. Это значит, что параметры заносятся в стек справа налево, вызывающая программа отвечает за их удаление из стека при выходе из функции и расширении ее имени. Расширение имен (name mangling) позволяет редактору связей различать перегруженные функции, т.е. функции с одинаковыми именами, но разными списками аргументов. Однако в старой библиотеке С функции с расширенными именами отсутствуют.

Хотя все остальные правила вызова функции в С идентичны правилам вызова функции в C++, в библиотеках С имена функций не расширяются. К ним только добавляется впереди символ подчеркивания (_).

Если необходимо подключить библиотеку на С к приложению на C++, все функции из этой библиотеки придется объявить как внешние в формате С:

Extern "С" int MyOldCFunction(int myParam);

Объявления функций библиотеки обычно помещаются в файле заголовка этой библиотеки, хотя заголовки большинства библиотек С не рассчитаны на применение в проектах на C++. В этом случае необходимо создать копию файла заголовка и включить в нее модификатор extern "C" к объявлению всех используемых функций библиотеки. Модификатор extern "C" можно применить и к целому блоку, к которому с помощью директивы #tinclude подключен файл старого заголовка С. Таким образом, вместо модификации каждой функции в отдельности можно обойтись всего тремя строками:

Extern "С" { #include "MyCLib.h" }

В программах для старых версий Windows использовались также соглашения о вызове функций языка PASCAL для функций Windows API. В новых программах следует использовать модификатор winapi, преобразуемый в _stdcall. Хотя это и не стандартный интерфейс функций С или C++, но именно он используется для обращений к функциям Windows API. Однако обычно все это уже учтено в стандартных заголовках Windows.

Загрузка неявно подключаемой DLL

При запуске приложение пытается найти все файлы DLL, неявно подключенные к приложению, и поместить их в область оперативной памяти, занимаемую данным процессом. Поиск файлов DLL операционной системой осуществляется в следующей последовательности.

  • Каталог, в котором находится ЕХЕ-файл.
  • Текущий каталог процесса.
  • Системный каталог Windows.

Если библиотека DLL не обнаружена, приложение выводит диалоговое окно с сообщением о ее отсутствии и путях, по которым осуществлялся поиск. Затем процесс отключается.

Если нужная библиотека найдена, она помещается в оперативную память процесса, где и остается до его окончания. Теперь приложение может обращаться к функциям, содержащимся в DLL.

Динамическая загрузка и выгрузка DLL

Вместо того, чтобы Windows выполняла динамическое связывание с DLL при первой загрузке приложения в оперативную память, можно связать программу с модулем библиотеки во время выполнения программы (при таком способе в процессе создания приложения не нужно использовать библиотеку импорта). В частности, можно определить, какая из библиотек DLL доступна пользователю, или разрешить пользователю выбрать, какая из библиотек будет загружаться. Таким образом можно использовать разные DLL, в которых реализованы одни и те же функции, выполняющие различные действия. Например, приложение, предназначенное для независимой передачи данных, сможет в ходе выполнения принять решение, загружать ли DLL для протокола TCP/IP или для другого протокола.

Загрузка обычной DLL

Первое, что необходимо сделать при динамической загрузке DLL, - это поместить модуль библиотеки в память процесса. Данная операция выполняется с помощью функции ::LoadLibrary , имеющей единственный аргумент - имя загружаемого модуля. Соответствующий фрагмент программы должен выглядеть так:

HINSTANCE hMyDll; :: if((hMyDll=:: LoadLibrary("MyDLL"))==NULL) { /* не удалось загрузить DLL */ } else { /* приложение имеет право пользоваться функциями DLL через hMyDll */ }

Стандартным расширением файла библиотеки Windows считает.dll, если не указать другое расширение. Если в имени файла указан и путь, то только он будет использоваться для поиска файла. В противном случае Windows будет искать файл по той же схеме, что и в случае неявно подключенных DLL, начиная с каталога, из которого загружается exe-файл, и продолжая в соответствии со значением PATH.

Когда Windows обнаружит файл, его полный путь будет сравнен с путем библиотек DLL, уже загруженных данным процессом. Если обнаружится тождество, вместо загрузки копии приложения возвращается дескриптор уже подключенной библиотеки.

Если файл обнаружен и библиотека успешно загрузилась, функция ::LoadLibrary возвращает ее дескриптор, который используется для доступа к функциям библиотеки.

Перед тем, как использовать функции библиотеки, необходимо получить их адрес. Для этого сначала следует воспользоваться директивой typedef для определения типа указателя на функцию и определить переменную этого нового типа, например:

// тип PFN_MyFunction будет объявлять указатель на функцию, // принимающую указатель на символьный буфер и выдающую значение типа int typedef int (WINAPI *PFN_MyFunction)(char *); :: PFN_MyFunction pfnMyFunction;

Затем следует получить дескриптор библиотеки, при помощи которого и определить адреса функций, например адрес функции с именем MyFunction:

HMyDll=::LoadLibrary("MyDLL"); pfnMyFunction=(PFN_MyFunction)::GetProcAddress(hMyDll,"MyFunction"); :: int iCode=(*pfnMyFunction)("Hello");

Адрес функции определяется при помощи функции ::GetProcAddress , ей следует передать имя библиотеки и имя функции. Последнее должно передаваться в том виде, в котором экспортируется из DLL.

Можно также сослаться на функцию по порядковому номеру, по которому она экспортируется (при этом для создания библиотеки должен использоваться def-файл, об этом будет рассказано далее):

PfnMyFunction=(PFN_MyFunction)::GetProcAddress(hMyDll, MAKEINTRESOURCE(1));

После завершения работы с библиотекой динамической компоновки, ее можно выгрузить из памяти процесса с помощью функции::FreeLibrary :

::FreeLibrary(hMyDll);

Загрузка MFC-расширений динамических библиотек

При загрузке MFC-расширений для DLL (подробно о которых рассказывается далее) вместо функций LoadLibrary и FreeLibrary используются функции AfxLoadLibrary и AfxFreeLibrary . Последние почти идентичны функциям Win32 API. Они лишь гарантируют дополнительно, что структуры MFC, инициализированные расширением DLL, не были запорчены другими потоками.

Ресурсы DLL

Динамическая загрузка применима и к ресурсам DLL, используемым MFC для загрузки стандартных ресурсов приложения. Для этого сначала необходимо вызвать функцию LoadLibrary и разместить DLL в памяти. Затем с помощью функции AfxSetResourceHandle нужно подготовить окно программы к приему ресурсов из вновь загруженной библиотеки. В противном случае ресурсы будут загружаться из файлов, подключенных к выполняемому файлу процесса. Такой подход удобен, если нужно использовать различные наборы ресурсов, например для разных языков.

Замечание. С помощью функции LoadLibrary можно также загружать в память исполняемые файлы (не запускать их на выполнение!). Дескриптор выполняемого модуля может затем использоваться при обращении к функциям FindResource и LoadResource для поиска и загрузки ресурсов приложения. Выгружают модули из памяти также при помощи функции FreeLibrary .

Пример обычной DLL и способов загрузки

Приведем исходный код динамически подключаемой библиотеки, которая называется MyDLL и содержит одну функцию MyFunction, которая просто выводит сообщение.

Сначала в заголовочном файле определяется макроконтстанта EXPORT . Использование этого ключевого слова при определении некоторой функции динамически подключаемой библиотеке позволяет сообщить компоновщику, что эта функция доступна для использования другими программами, в результате чего он заносит ее в библиотеку импорта. Кроме этого, такая функция, точно так же, как и оконная процедура, должна определяться с помощью константы CALLBACK :

MyDLL.h #define EXPORT extern "C" __declspec (dllexport) EXPORT int CALLBACK MyFunction(char *str);

Файл библиотеки также несколько отличается от обычных файлов на языке C для Windows. В нем вместо функции WinMain имеется функция DllMain . Эта функция используется для выполнения инициализации, о чем будет рассказано позже. Для того, чтобы библиотека осталась после ее загрузки в памяти, и можно было вызывать ее функции, необходимо, чтобы ее возвращаемым значением было TRUE:

MyDLL.c #include #include "MyDLL.h" int WINAPI DllMain(HINSTANCE hInstance, DWORD fdReason, PVOID pvReserved) { return TRUE; } EXPORT int CALLBACK MyFunction(char *str) { MessageBox(NULL,str,"Function from DLL",MB_OK); return 1; }

После трансляции и компоновки этих файлов появляется два файла - MyDLL.dll (сама динамически подключаемая библиотека) и MyDLL.lib (ее библиотека импорта).

Пример неявного подключения DLL приложением

Приведем теперь исходный код простого приложения, которое использует функцию MyFunction из библиотеки MyDLL.dll:

#include #include "MyDLL.h" int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { int iCode=MyFunction("Hello"); return 0; }

Эта программа выглядит как обычная программ для Windows, чем она в сущности и является. Тем не менее, следует обратить внимание, что в исходный ее текст помимо вызова функции MyFunction из DLL-библиотеки включен и заголовочный файл этой библиотеки MyDLL.h. Также необходимо на этапе компоновки приложения подключить к нему библиотеку импорта MyDLL.lib (процесс неявного подключения DLL к исполняемому модулю).

Чрезвычайно важно понимать, что сам код функции MyFunction не включается в файл MyApp.exe. Вместо этого там просто имеется ссылка на файл MyDLL.dll и ссылка на функцию MyFunction, которая находится в этом файле. Файл MyApp.exe требует запуска файла MyDLL.dll.

Заголовочный файл MyDLL.h включен в файл с исходным текстом программы MyApp.c точно так же, как туда включен файл windows.h. Включение библиотеки импорта MyDLL.lib для компоновки аналогично включению туда всех библиотек импорта Windows. Когда программа MyApp.exe работает, она подключается к библиотеке MyDLL.dll точно так же, как ко всем стандартным динамически подключаемым библиотекам Windows.

Пример динамической загрузки DLL приложением

Приведем теперь полностью исходный код простого приложения, которое использует функцию MyFunction из библиотеки MyDLL.dll, используя динамическую загрузку библиотеки:

#include typedef int (WINAPI *PFN_MyFunction)(char *); int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { HINSTANCE hMyDll; if((hMyDll=LoadLibrary("MyDLL"))==NULL) return 1; PFN_MyFunction pfnMyFunction; pfnMyFunction=(PFN_MyFunction)GetProcAddress(hMyDll,"MyFunction"); int iCode=(*pfnMyFunction)("Hello"); FreeLibrary(hMyDll); return 0; }

Создание DLL

Теперь, познакомившись с принципами работы библиотек DLL в приложениях, рассмотрим способы их создания. При разработке приложении функции, к которым обращается несколько процессов, желательно размещать в DLL. Это позволяет более рационально использовать память в Windows.

Проще всего создать новый проект DLL с помощью мастера AppWizard, который автоматически выполняет многие операции. Для простых DLL, таких как рассмотренные в этой главе, необходимо выбрать тип проекта Win32 Dynamic-Link Library. Новому проекту будут присвоены все необходимые параметры для создания библиотеки DLL. Файлы исходных текстов придется добавлять к проекту вручную.

Если же планируется в полной мере использовать функциональные возможности MFC, такие как документы и представления, или намерены создать сервер автоматизации OLE, лучше выбрать тип проекта MFC AppWizard (dll). В этом случае, помимо присвоения проекту параметров для подключения динамических библиотек, мастер проделает некоторую дополнительную работу. В проект будут добавлены необходимые ссылки на библиотеки MFC и файлы исходных текстов, содержащие описание и реализацию в библиотеке DLL объекта класса приложения, производного от CWinApp .

Иногда удобно сначала создать проект типа MFC AppWizard (dll) в качестве тестового приложения, а затем - библиотеку DLL в виде его составной части. В результате DLL в случае необходимости будет создаваться автоматически.

Функция DllMain

Большинство библиотек DLL - просто коллекции практически независимых друг от друга функций, экспортируемых в приложения и используемых в них. Кроме функций, предназначенных для экспортирования, в каждой библиотеке DLL есть функция DllMain . Эта функция предназначена для инициализации и очистки DLL. Она пришла на смену функциям LibMain и WEP , применявшимся в предыдущих версиях Windows. Структура простейшей функции DllMain может выглядеть, например, так:

BOOL WINAPI DllMain (HANDLE hInst,DWORD dwReason, LPVOID IpReserved) { BOOL bAllWentWell=TRUE; switch (dwReason) { case DLL_PROCESS_ATTACH: // Инициализация процесса. break; case DLL_THREAD_ATTACH: // Инициализация потока. break; case DLL_THREAD_DETACH: // Очистка структур потока. break; case DLL_PROCESS_DETACH: // Очистка структур процесса. break; } if(bAllWentWell) return TRUE; else return FALSE; }

Функция DllMain вызывается в нескольких случаях. Причина ее вызова определяется параметром dwReason, который может принимать одно из следующих значений.

При первой загрузке библиотеки DLL процессом вызывается функция DllMain с dwReason, равным DLL_PROCESS_ATTACH. Каждый раз при создании процессом нового потока DllMainO вызывается с dwReason, равным DLL_THREAD_ATTACH (кроме первого потока, потому что в этом случае dwReason равен DLL_PROCESS_ATTACH).

По окончании работы процесса с DLL функция DllMain вызывается с параметром dwReason, равным DLL_PROCESS_DETACH. При уничтожении потока (кроме первого) dwReason будет равен DLL_THREAD_DETACH.

Все операции по инициализации и очистке для процессов и потоков, в которых нуждается DLL, необходимо выполнять на основании значения dwReason, как было показано в предыдущем примере. Инициализация процессов обычно ограничивается выделением ресурсов, совместно используемых потоками, в частности загрузкой разделяемых файлов и инициализацией библиотек. Инициализация потоков применяется для настройки режимов, свойственных только данному потоку, например для инициализации локальной памяти.

В состав DLL могут входить ресурсы, не принадлежащие вызывающему эту библиотеку приложению. Если функции DLL работают с ресурсами DLL, было бы, очевидно, полезно сохранить где-нибудь в укромном месте дескриптор hInst и использовать его при загрузке ресурсов из DLL. Указатель IpReserved зарезервирован для внутреннего использования Windows. Следовательно, приложение не должно претендовать на него. Можно лишь проверить его значение. Если библиотека DLL была загружена динамически, оно будет равно NULL. При статической загрузке этот указатель будет ненулевым.

В случае успешного завершения функция DllMain должна возвращать TRUE. В случае возникновения ошибки возвращается FALSE, и дальнейшие действия прекращаются.

Замечание. Если не написать собственной функции DllMain(), компилятор подключит стандартную версию, которая просто возвращает TRUE.

Экспортирование функций из DLL

Чтобы приложение могло обращаться к функциям динамической библиотеки, каждая из них должна занимать строку в таблице экспортируемых функций DLL. Есть два способа занести функцию в эту таблицу на этапе компиляции.

Метод __declspec (dllexport)

Можно экспортировать функцию из DLL, поставив в начале ее описания модификатор __declspec (dllexport) . Кроме того, в состав MFC входит несколько макросов, определяющих __declspec (dllexport), в том числе AFX_CLASS_EXPORT, AFX_DATA_EXPORT и AFX_API_EXPORT.

Метод __declspec применяется не так часто, как второй метод, работающий с файлами определения модуля (.def), и позволяет лучше управлять процессом экспортирования.

Файлы определения модуля

Синтаксис файлов с расширением.def в Visual C++ достаточно прямолинеен, главным образом потому, что сложные параметры, использовавшиеся в ранних версиях Windows, в Win32 более не применяются. Как станет ясно из следующего простого примера, .def-файл содержит имя и описание библиотеки, а также список экспортируемых функций:

MyDLL.def LIBRARY "MyDLL" DESCRIPTION "MyDLL - пример DLL-библиотеки" EXPORTS MyFunction @1

В строке экспорта функции можно указать ее порядковый номер, поставив перед ним символ @. Этот номер будет затем использоваться при обращении к GetProcAddress (). На самом деле компилятор присваивает порядковые номера всем экспортируемым объектам. Однако способ, которым он это делает, отчасти непредсказуем, если не присвоить эти номера явно.

В строке экспорта можно использовать параметр NONAME. Он запрещает компилятору включать имя функции в таблицу экспортирования DLL:

MyFunction @1 NONAME

Иногда это позволяет сэкономить много места в файле DLL. Приложения, использующие библиотеку импортирования для неявного подключения DLL, не "заметят" разницы, поскольку при неявном подключении порядковые номера используются автоматически. Приложениям, загружающим библиотеки DLL динамически, потребуется передавать в GetProcAddress порядковый номер, а не имя функции.

При использовании вышеприведенного def-файл описания экспортируемых функций DLL-библиотеки может быть,например, не таким:

#define EXPORT extern "C" __declspec (dllexport) EXPORT int CALLBACK MyFunction(char *str); a таким: extern "C" int CALLBACK MyFunction(char *str);

Экспортирование классов

Создание.def-файла для экспортирования даже простых классов из динамической библиотеки может оказаться довольно сложным делом. Понадобится явно экспортировать каждую функцию, которая может быть использована внешним приложением.

Если взглянуть на реализованный в классе файл распределения памяти, в нем можно заметить некоторые весьма необычные функции. Оказывается, здесь есть неявные конструкторы и деструкторы, функции, объявленные в макросах MFC, в частности _DECLARE_MESSAGE_MAP, а также функции, которые написанные программистом.

Хотя можно экспортировать каждую из этих функций в отдельности, есть более простой способ. Если в объявлении класса воспользоваться макромодификатором AFX_CLASS_EXPORT, компилятор сам позаботится об экспортировании необходимых функций, позволяющих приложению использовать класс, содержащийся в DLL.

Память DLL

В отличие от статических библиотек, которые, по существу, становятся частью кода приложения, библиотеки динамической компоновки в 16-разрядных версиях Windows работали с памятью несколько иначе. Под управлением Win 16 память DLL размещалась вне адресного пространства задачи. Размещение динамических библиотек в глобальной памяти обеспечивало возможность совместного использования их различными задачами.

В Win32 библиотека DLL располагается в области памяти загружающего ее процесса. Каждому процессу предоставляется отдельная копия "глобальной" памяти DLL, которая реинициализируется каждый раз, когда ее загружает новый процесс. Это означает, что динамическая библиотека не может использоваться совместно, в общей памяти, как это было в Winl6.

И все же, выполнив ряд замысловатых манипуляций над сегментом данных DLL, можно создать общую область памяти для всех процессов, использующих данную библиотеку.

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

#pragma data_seg(".myseg") int sharedlnts ; // другие переменные общего пользования #pragma data_seg() #pragma comment(lib, "msvcrt" "-SECTION:.myseg,rws");

Все переменные, объявленные между директивами #pragma data_seg(), размещаются в сегменте.myseg. Директива #pragma comment () - не обычный комментарий. Она дает указание библиотеке выполняющей системы С пометить новый раздел как разрешенный для чтения, записи и совместного доступа.

Полная компиляция DLL

Если проект динамической библиотеки создан с помощью AppWizard и.def-файл модифицирован соответствующим образом - этого достаточно. Если же файлы проекта создаются вручную или другими способами без помощи AppWizard, в командную строку редактора связей следует включить параметр /DLL. В результате вместо автономного выполняемого файла будет создана библиотека DLL.

Если в.def-файле есть строка LIBRART, указывать явно параметр /DLL в командной строке редактора связей не нужно.

Для MFC предусмотрен ряд особых режимов, касающихся использования динамической библиотекой библиотек MFC. Этому вопросу посвящен следующий раздел.

DLL и MFC

Программист не обязан использовать MFC при создании динамических библиотек. Однако использование MFC открывает ряд очень важных возможностей.

Имеется два уровня использования структуры MFC в DLL. Первый из них - это обычная динамическая библиотека на основе MFC, MFC DLL (regular MFC DLL). Она может использовать MFC, но не может передавать указатели на объекты MFC между DLL и приложениями. Второй уровень реализован в динамических расширениях MFC (MFC extensions DLL). Использование этого вида динамических библиотек требует некоторых дополнительных усилий по настройке, но позволяет свободно обмениваться указателями на объекты MFC между DLL и приложением.

Обычные MFC DLL

Обычные MFC DLL позволяют применять MFC в динамических библиотеках. При этом приложения, обращающиеся к таким библиотекам, не обязательно должны быть построены на основе MFC. В обычных DLL можно использовать MFC любым способом, в том числе создавая в DLL новые классы на базе классов MFC и экспортируя их в приложения.

Однако обычные DLL не могут обмениваться с приложениями указателями на классы, производные от MFC.

Если приложению необходимо обмениваться с DLL указателями на объекты классов MFC или их производных, нужно использовать расширение DLL, описанное в следующем разделе.

Архитектура обычных DLL рассчитана на использование другими средами программирования, такими как Visual Basic и PowerBuilder.

При создании обычной библиотеки MFC DLL с помощью AppWizard выбирается новый проект типа MFC AppWizard (dll) . В первом диалоговом окне мастера приложений необходимо выбрать один из режимов для обычных динамических библиотек: "Regular DLL with MFC statistically linked" или "Regular DLL using shared MFC DLL". Первый предусматривает статическое, а второй - динамическое подключение библиотек MFC. Впоследствии режим подключения MFC к DLL можно будет изменить с помощью комбинированного списка на вкладке "General" диалогового окна "Project settings".

Управление информацией о состоянии MFC

В каждом модуле процесса MFC содержится информация о его состоянии. Таким образом, информация о состоянии DLL отлична от информации о состоянии вызвавшего ее приложения. Поэтому любые экспортируемые из библиотеки функции, обращение к которым исходит непосредственно из приложений, должны сообщать MFC, какую информацию состояния использовать. В обычной MFC DLL, использующей динамические библиотеки MFC, перед вызовом любой подпрограммы MFC в начале экспортируемой функции нужно поместить следующую строку:

AFX_MANAGE_STATE(AfxGetStaticModuleState()) ;

Данный оператор определяет использование соответствующей информации о состоянии во время выполнения функции, обратившейся к данной подпрограмме.

Динамические расширения MFC

MFC позволяет создавать такие библиотеки DLL, которые воспринимаются приложениями не как набор отдельных функций, а как расширения MFC. С помощью данного вида DLL можно создавать новые классы, производные от классов MFC, и использовать их в своих приложениях.

Чтобы обеспечить возможность свободного обмена указателями на объекты MFC между приложением и DLL, нужно создать динамическое расширение MFC. DLL этого типа подключаются к динамическим библиотекам MFC так же, как и любые приложения, использующие динамическое расширение MFC.

Чтобы создать новое динамическое расширение MFC, проще всего, воспользовавшись мастером приложении, присвоить проекту тип MFC AppWizard (dll) и на шаге 1 включить режим "MFC Extension DLL". В результате новому проекту будут присвоены все необходимые атрибуты динамического расширения MFC. Кроме того, будет создана функция DllMain для DLL, выполняющая ряд специфических операций по инициализации расширения DLL. Следует обратить внимание, что динамические библиотеки данного типа не содержат и не должны содержать объектов, производных от CWinApp .

Инициализация динамических расширений

Чтобы "вписаться" в структуру MFC, динамические расширения MFC требуют дополнительной начальной настройки. Соответствующие операции выполняются функцией DllMain . Рассмотрим пример этой функции, созданный мастером AppWizard.

Static AFX_EXTENSION_MODULE MyExtDLL = { NULL, NULL } ; extern "C" int APIENTRY DllMain(HINSTANCE hinstance, DWORD dwReason, LPVOID IpReserved) { if (dwReason == DLL_PROCESS_ATTACH) { TRACED("MYEXT.DLL Initializing!\n") ; // Extension DLL one-time initialization AfxInitExtensionModule(MyExtDLL, hinstance) ; // Insert this DLL into the resource chain new CDynLinkLibrary(MyExtDLL); } else if (dwReason == DLL_PROCESS_DETACH) { TRACED("MYEXT.DLL Terminating!\n") ; } return 1; // ok }

Самой важной частью этой функции является вызов AfxInitExtensionModule . Это инициализация динамической библиотеки, позволяющая ей корректно работать в составе структуры MFC. Аргументами данной функции являются передаваемый в DllMain дескриптор библиотеки DLL и структура AFX_EXTENSION_MODULE, содержащая информацию о подключаемой к MFC динамической библиотеке.

Нет необходимости инициализировать структуру AFX_EXTENSION_MODULE явно. Однако объявить ее нужно обязательно. Инициализацией же займется конструктор CDynLinkLibrary . В DLL необходимо создать класс CDynLinkLibrary . Его конструктор не только будет инициализировать структуру AFX_EXTENSION_MODULE, но и добавит новую библиотеку в список DLL, с которыми может работать MFC.

Загрузка динамических расширений MFC

Начиная с версии 4.0 MFC позволяет динамически загружать и выгружать DLL, в том числе и расширения. Для корректного выполнения этих операций над создаваемой DLL в ее функцию DllMain в момент отключения от процесса необходимо добавить вызов AfxTermExtensionModule . Последней функции в качестве параметра передается уже использовавшаяся выше структура AFX_EXTENSION_MODULE. Для этого в текст DllMain нужно добавить следующие строки.

If(dwReason == DLL_PROCESS_DETACH) { AfxTermExtensionModule(MyExtDLL); }

Кроме того, следует помнить, что новая библиотека DLL является динамическим расширением и должна загружаться и выгружаться динамически, с помощью функций AfxLoadLibrary и AfxFreeLibrary ,а не LoadLibrary и FreeLibrary .

Экспортирование функций из динамических расширений

Рассмотрим теперь, как осуществляется экспортирование в приложение функций и классов из динамического расширения. Хотя добавить в DEF-файл все расширенные имена можно и вручную, лучше использовать модификаторы для объявлений экспортируемых классов и функций, такие как AFX_EXT_CLASS и AFX_EXT_API,например:

Class AFX_EXT_CLASS CMyClass: public CObject (// Your class declaration } void AFX_EXT_API MyFunc() ;

Поддержите проект — поделитесь ссылкой, спасибо!
Читайте также
Виртуальная реальность vs Дополненная реальность Что такое VR и AR Виртуальная реальность vs Дополненная реальность Что такое VR и AR Какой сервис даёт наибольшее бесплатное пространство на облаке Какой сервис даёт наибольшее бесплатное пространство на облаке Лучшая программа для обновления драйверов Лучшая программа для обновления драйверов