Создание и использование динамических библиотек

Материал из ПИЭ.Wiki

Перейти к: навигация, поиск

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

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

Содержание

Создание DLL в CodeBlocks

Для создания DLL-проекта в среде разработки CodeBlocks при создании нового проекта необходимо выбрать пункт «Dynamic Link Library». Созданный проект будет содержать два файла main.cpp и main.h.

Файл main.cpp

#include "main.h"

// a sample exported function
void DLL_EXPORT SomeFunction(const LPCSTR sometext)
{
    MessageBoxA(0, sometext, "DLL Message", MB_OK | MB_ICONINFORMATION);
}

BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved)
{
    switch (fdwReason)
    {
        case DLL_PROCESS_ATTACH:
            // attach to process
            // return FALSE to fail DLL load
            break;

        case DLL_PROCESS_DETACH:
            // detach from process
            break;

        case DLL_THREAD_ATTACH:
            // attach to thread
            break;

        case DLL_THREAD_DETACH:
            // detach from thread
            break;
    }
    return TRUE; // succesful
}

Файл main.h

#ifndef __MAIN_H__
#define __MAIN_H__

#include <windows.h>

/*  To use this exported function of dll, include this header
 *  in your project.
 */

#ifdef BUILD_DLL
    #define DLL_EXPORT __declspec(dllexport)
#else
    #define DLL_EXPORT __declspec(dllimport)
#endif

Для экспортирования функции из DLL - перед ее описанием следует указать ключевое слово __declspec(dllexport).

Точка входа DLL

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

Система при вызове функции передаёт следующие параметры:

  • hinstDLL - ссылка на DLL. В действительности для 32-х разрядных библиотек этот параметр содержит базовый (начальный) адрес по которому загружена библиотека в виртуальном адресном пространстве процесса.
  • fdwReason - содержит код события которое было причиной вызова функции.
  • LpvReserved - зарезервирован и не используется.

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

DLL_PROCESS_ATTACH новый процесс загружает динамическую библиотеку автоматически или функцией LoadLibrary.

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

DLL_THREAD_ATTACH в одном из процессов использующих DLL создан новый поток.

DLL_THREAD_DETACH в одном из процессов использующих DLL завершился поток.

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

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

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

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

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

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

Загрузка DLL с неявной компоновкой

Чтобы вызвать функцию из DLL, ее необходимо объявить в вызывающем коде - либо как external (т.е. как обычную внешнюю функцию), либо предварить ключевым словом __declspec(dllimport). Первый способ более популярен, но второй все же предпочтительнее - в этом случае компилятор, поняв, что функция вызывается именно из DLL, сможет соответствующим образом оптимизировать код. Например:

  // ImplictDll.c
  // Объявляем внешнюю функцию SomeFunction
  __declspec(dllimport) void SomeFunction(const LPCSTR sometext)

  main()
  {
    // Вызываем функцию Demo из DLL
    Demo("Hello, World!\n");
    return 0;
  }

Загрузка DLL с явной компоновкой

Явную загрузку динамических библиотек осуществляет функция

HINSTANCE LoadLibrary(LPCTSTR lpLibFileName)

или ее расширенный аналог

HINSTANCE LoadLibraryEx(LPCTSTR lpLibFileName, HANDLE hFile, DWORD dwFlags)

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

FARPROC GetProcAddress(HMODULE hModule, LPCSTR lpProcName)

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

BOOL FreeLibrary(HMODULE hLibModule)

Для пояснения приведем код примера с подробными комментариями:

// DynCall.c
#include <stdio.h>
#include <windows.h>

main()
{
  // Дескриптор загружаемой dll
  HINSTANCE h;

  // Объявление указателя на функцию, вызываемой из DLL
  // Обратите внимание – имена объявляемой функции и
  // функции, вызываемой из DLL, могут и не совпадать,
  // т.к. за выбор вызываемой функции отвечает
  // GetProcAddress
  void (*DllFunc) (char *str);

  // Загружаем MyFirstDLL
  h=LoadLibrary("MyFirstDLL.dll");

  // Контроль ошибок – если загрузка прошла успешно,
  // функция вернет что-то отличное от нуля
  if (!h)
  {
    printf("Ошибка - не могу найти MyFirstDLL.dll\n");
    return;
  }

  // Вызовом GetProcAddress получаем адрес функции Demo
  // и присваиваем его указателю DllFunc с явным 
  // приведением типов. Это необходимо т.к.
  // GetProcAddress возвращает бестиповой far-указатель
  DllFunc=(void (*) (char *str))
            GetProcAddress(h,"Demo");

  // Контроль ошибок – если вызов функции GetProcAddress
  // завершился успешно, она вернет ненулевой указатель
  if (!DllFunc)
  {
    printf("Ошибка! В MyFirstDLL "
           "отсутствует ф-ция   Demo\n");
    return;
  }

  // Вызов функции Demo из DLL
  DllFunc("Test");

  // Выгрузка динамической библиотеки из памяти
  FreeLibrary(h);
}

Выгрузка динамических библиотек из памяти

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

BOOL FreeLibrary(HMODULE hLibModule)

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

Задержка выгрузки предусмотрена специально - на тот случай, если эта же DLL через некоторое время вновь понадобится какому-то процессу. Такой трюк оптимизирует работу часто используемых динамических библиотек, но плохо подходит для редко используемых DLL, загружаемых лишь однажды на короткое время. Никаких документированных способов насильно выгрузить динамическую библиотеку из памяти нет; а те, что есть - работают с ядром на низком уровне и не могут похвастаться переносимостью. Поэтому здесь мы их рассматривать не будем. К тому же - тактика освобождения и выгрузки DLL по-разному реализована в каждой версии Windows: Microsoft, стремясь подобрать наилучшую стратегию, непрерывно изменяет этот алгоритм; а потому и отказывается его документировать. Нельзя не обратить внимания на одно очень важное обстоятельство: динамическая библиотека не владеет никакими ресурсами - ими владеет, независимо от способа компоновки, загрузивший ее процесс. Динамическая библиотека может открывать файлы, выделять память и т. д., но память не будет автоматически освобождена после вызова FreeLibrary, а файлы не окажутся сами собой закрыты - все это произойдет лишь после завершения процесса, но не раньше! Естественно, если программист сам не освободит все ненужные ресурсы вручную, с помощью функций CloseHandle, FreeMemory и подобных им.

Если функция FreeLibrary пропущена, DLL освобождается (но не факт, что выгружается!) только после завершения вызвавшего процесса. Могут возникнуть сомнения: раз FreeLibrary немедленно не выгружает динамическую библиотеку из памяти, так зачем она вообще нужна? Не лучше ли тогда все пустить на самотек - все равно ведь загруженные DLL будут гарантированно освобождены после завершения процесса? Что ж, доля правды тут есть, и автор сам порой так и поступает; но при недостатке памяти операционная система может беспрепятственно использовать место, занятое освобожденными динамическими библиотеками под что-то полезное - а если DLL еще не освобождены, их придется "скидывать" в файл подкачки, теряя драгоценное время. Поэтому лучше освобождайте DLL сразу же после их использования!

Просмотры
Инструменты

Besucherzahler russian mail order brides
счетчик посещений
Rambler's Top100
Лингафонные кабинеты  Интерактивные доски  Интерактивная приставка Mimio Teach