Dll windows visual studio

Использование DLL в программе на Visual C++


Автор: Александр Шаргин


Версия текста: 2.0

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

При неявном подключении (implicit linking) линкеру передается библиотека импорта (обычно имеет расширение lib), содержащая список переменных и функций DLL, которые могут использовать приложения. Обнаружив, что программа обращается хотя бы к одной из них, линкер добавляет в целевой exe-файл таблицу импорта . Таблица импорта содержит список всех DLL, которые использует программа, с указанием конкретных переменных и функций, к которым она обращается. Позже, когда exe-файл будет запущен, загрузчик проецирует все DLL, перечисленные в таблице импорта, на адресное пространство процесса; в случае неудачи весь процесс немедленно завершается.

При явном подключении (explicit linking) приложение вызывает функцию LoadLibrary, чтобы загрузить DLL, затем использует функцию GetProcAddress, чтобы получить указатели на требуемые функции (или переменные), а по окончании работы с ними вызывает FreeLibrary, чтобы выгрузить библиотеку и освободить занимаемые ею ресурсы.

Каждый из способов имеет свои достоинства и недостатки. В случае неявного подключения все библиотеки, используемые приложением, загружаются в момент его запуска и остаются в памяти до его завершения (даже если другие запущенные приложения их не используют). Это может привести к нерациональному расходу памяти, а также заметно увеличить время загрузки приложения, если оно использует очень много различных библиотек. Кроме того, если хотя бы одна из неявно подключаемых библиотек отсутствует, работа приложения будет немедленно завершена. Явный метод лишен этих недостатков, но делает программирование более неудобным, поскольку требуется следить за своевременными вызовами LoadLibrary и соответствующими им вызовами FreeLibrary, а также получать адрес каждой функции через вызов GetProcAddress.

В Visual C++ 6.0 появился еще один способ подключения DLL, сочетающий в себе почти все достоинства двух рассмотренных ранее методов — отложенная загрузка DLL (delay-load DLL). Отложенная загрузка не требует поддержки со стороны операционной системы (а значит будет работать даже под Windows 95), а реализуется линкером Visual C++ 6.0. При отложенной загрузке DLL загружается только тогда, когда приложение обращается к одной из содержащихся в ней функций. Это происходит незаметно для программиста (то есть вызывать LoadLibrary/GetProcAddress не требуется). После того как работа с функциями библиотеки завершена, ее можно оставить в памяти или выгрузить посредством функции __FUnloadDelayLoadedDLL. Вызов этой функции — единственная модификация кода, которую может потребоваться сделать программисту (по сравнению с неявным подключением DLL). Если требуемая DLL не обнаружена, приложение аварийно завершается, но и здесь ситуацию можно исправить, перехватив исключение с помощью конструкции __try/__except. Как видим, отложенная загрузка DLL — весьма удобное средство для программиста.

Теперь рассмотрим, как каждый из перечисленных методов используется на практике. Для этого будем считать, что у нас есть библиотека MyDll.dll, которая экспортирует переменную Var, функцию Function и класс Class. Их объявления содержатся в заголовочном файле MyDll.h, который выглядит следующим образом:

Кроме того, будем считать, что библиотека импорта содержится в файле MyDll.lib.

Неявное подключение

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

Во-первых, можно непосредственно добавить файл MyDll.lib в проект посредством команды Project->Add to project->Files. Во-вторых, можно указать имя библиотеки импорта в опциях линкера. Для этого откройте окно настроек проекта (Project->Settings. ) и добавьте в поле Object/Library modules на вкладке Link имя MyDll.lib. Наконец, можно встроить ссылку на библиотеку импорта прямо в исходный код программы. Для этого используется директива #pragma c ключем comment. В нашем случае необходимо вставить в программу строчку:

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

Явное подключение


Загрузка DLL

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

В нашем примере загрузка DLL выглядит так.

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

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

Читайте также:  Разрешить запуск системных служб windows 10

Обратите внимание на приведение указателя к ссылке на тип FARPROC. FARPROC — это указатель на функцию, которая не принимает параметров и возвращает int. Именно такой указатель возвращает функция GetProcAddress. Приведение типа необходимо, чтобы умиротворить компилятор, который строго следит за соответствием типов параметров оператора присваивания. Альтернативный подход заключается в использовании оператора typedef с последующим приведением значения, возвращаемого GetProcAddress, к указателю на функцию с нужным прототипом.

Доступ к переменным

Хотя это не всегда очевидно из документации, получить указатель на переменную из DLL можно, используя все ту же функцию GetProcAddress. В нашем примере это выглядит так.

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

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

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

Сначала задумаемся, почему объекты класса из явно подключаемой библиотеки нельзя использовать, как обычно. Дело в том, что при создании объекта класса компилятор генерирует вызов его конструктора. Но линкер не может разрешить этот вызов, поскольку адрес конструктора будет известен только в процессе выполнения программы. В результате сборка программы закончится неудачно. Такая же проблема возникает при вызове невиртуальных методов класса. С другой стороны, вызов виртуальных методов возможен, так как он осуществляется через таблицу виртуальных функций (vtable). Так, следующий фрагмент откомпилируется и слинкуется нормально (хотя, конечно, вызовет ошибку в процессе выполнения):

Приведенные выше рассуждения подсказывают решение проблемы. Коль скоро неявный вызов конструктра невозможен, мы можем вызвать его вручную, предварительно получив его адрес и выделив память под объект. Затем можно вызывать невиртуальные методы, получая их адреса с помощью GetProcAddress. Виртуальные методы можно вызывать, как обычно. Кроме того, необходимо не забыть вручную вызвать деструктор объекта, прежде чем выделенная для него память будет освобождена.

Продемонстрирую все сказанное на примере. Сначала мы выделяем память для объекта и вызываем для него конструктор. Память можно выделить как на стеке, так и в куче (с помощью оператора new). Рассмотрим оба варианта.

Обратите внимание на использование операторов .* и ->* для вызова функции-члена класса по указателю на нее. Этими операторами мы будем пользоваться и дальше.

ПРИМЕЧАНИЕ

Как правило, имена функций, экспортируемых из DLL, искажаются линкером. Поэтому вместо понятного имени, такого как «Constructor», получается совершенно нечитабельное имя вида «??0Class@@QAE@XZ». В рассматриваемом примере я назначил переменным и функциям нормальные имена при помощи def-файла следующего содержания:

Невиртуальные методы класса вызываются так же, как и конструктор, например:

Виртуальные методы вызываются непосредственно (как это делается для обычных классов). Хотя DLL и экспортирует их, явно получать их адреса с помощью GetProcAddress не требуется. Отсюда следует вывод: если все методы класса являются виртуальными, использование объектов класса из явно подключаемой библиотеки практически ничем не отличается от использования объектов любого другого класса. Разница только в том, что конструктор и деструктор для таких объектов придется вызывать вручную.

В нашем примере виртуальная функция вызывается так.

После того, как работа с объектом завершена, его нужно уничтожить, вызвав для него деструктор. Если объект был создан на стеке, деструктор необходимо вызвать до его выхода из области видимости, иначе возможны неприятные последствия (например, утечки памяти). Если объект был распределен при помощи new, его необходимо уничтожить перед вызовом delete. В нашем примере это выглядит так.

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

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

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

Отложенная загрузка


Использование отложенной загрузки

Чтобы линкер мог встроить в программу функцию отложенной загрузки, ему необходимо передать библиотеку импорта DLL, а также статическую библиотеку Delayimp.lib, в которой содержатся вспомогательные функции механизма отложенной загрузки. Сделать это можно любым из способов, которые обсуждались в разделе о неявном подключении. Кроме того, нужно передать линкеру ключ /DELAYLOAD: , сообщающий о нашем желании отложить загрузку DLL до фактического обращения к одной из ее функций. Этот ключ можно добавить в настройки проекта или встроить прямо в исходный код программы, используя директиву #pragma. Вот как это выглядит в нашем примере.

Вот и все. Теперь можно использовать функции и классы DLL прозрачно, как и в случае с неявным подключением. Единственная проблема возникает с переменными: их невозможно использовать напрямую. Дело в том, что при обращении к одной из функций в DLL мы на самом деле вызываем функцию __delayLoadHelper, которая и выполняет загрузку DLL (если она еще не загружена), затем получает адрес функции с помощью GetProcAddress и перенаправляет все последующие вызовы функции по этому адресу. Но при обращении к переменной вызова функции не происходит, а значит использовать __delayLoadHelper не удается.

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

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

Итак, мы установили, что при использовании отложенной загрузки DLL грузится в память, когда происходит обращение к одной из ее функций. Но в последствии нам может потребоваться выгрузить ее, чтобы не занимать зря системные ресурсы. Специально для этого предназначена функция __FUnloadDelayLoadedDLL, объявленная в файле Delayimp.h. Если вы планируете использовать ее, вам нужно задать еще один ключ линкера — /DELAY:UNLOAD. Например:

Имя, которое вы передаете функции __FUnloadDelayLoadedDLL, должно в точности соответствовать имени, заданному в ключе /DELAYLOAD. Если, к примеру, передать ей «MYLIB.DLL» или «mylib.dll», библиотека останется в памяти.

ПРЕДУПРЕЖДЕНИЕ

Не используйте FreeLibrary, чтобы выгрузить DLL с отложенной загрузкой.

Обработка исключений

Как я уже говорил, в случае ошибки функция __delayLoadHelper возбуждает исключение. Если нужная DLL не обнаружена, возбуждается исключение с кодом VcppException(ERROR_SEVERITY_ERROR, ERROR_MOD_NOT_FOUND). Если в DLL не обнаружена требуемая функция, исключение будет иметь код VcppException(ERROR_SEVERITY_ERROR, ERROR_PROC_NOT_FOUND).

ПРИМЕЧАНИЕ

VcppException — это макрос, который используется для формирования кода ошибки в подсистеме Visual C++. Первый параметр задает «степень серьезности» ошибки, а второй — код исключения.

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

Написанный вами фильтр исключений может также получить дополнительную информацию с помощью функции GetExceptionInformation. Эта функция возвращает указатель на структуру EXCEPTION_POINTERS. В ней содержится поле ExceptionRecord — указатель на структуру EXCEPTION_RECORD. А структура EXCEPTION_RECORD в свою очередь содержит поле ExceptionInformation[0], в которое __delayLoadHelper помещает указатель на структуру DelayLoadInfo, содержащую дополнительную информацию. Эта структура объявлена следующим образом (файл Delayimp.h).

В частности, вы можете извлечь из нее имя DLL (поле szDll), а также имя или порядковый номер функции, вызов которой привел к исключению (поле dlp).

Функции-ловушки

Если возможностей обработки исключений вам недостаточно, вы можете пойти еще дальше и вмешаться в работу __delayLoadHelper, используя функции-ловушки. В Visual C++ предусмотрено две таких функции: одна из них получает уведомления, вторая — сообщения об ошибках. Чтобы функция __delayLoadHelper могла обращаться к ним, их адреса нужно записать в глобальные переменные __pfnDliNotifyHook и __pfnDliFailureHook соответственно. Иногда обе функции объединяют в одну.

Функции-ловушки должны иметь следующий прототип:

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

В качестве примера приведу текст функции-ловушки, которая подменяет вызов функции SomeFunc на вызов функции YetAnotherFunc.

Практическое руководство. Отладка из проекта DLL в Visual Studio (C#, C++, Visual Basic, F#) How to: Debug from a DLL project in Visual Studio (C#, C++, Visual Basic, F#)

Одним из способов отладки проекта библиотеки DLL является указание вызывающего приложения в свойствах проекта библиотеки DLL. One way to debug a DLL project is to specify the calling app in the DLL project properties. Затем можно запускать отладку из самого проекта DLL. Then you can start debugging from the DLL project itself. Чтобы этот метод работал, приложение должно вызвать одну и ту же библиотеку DLL в том расположении, которое вы настроили. For this method to work, the app must call the same DLL in the same location as the one you configure. Если приложение находит и загружает другую версию библиотеки DLL, эта версия не будет содержать точки останова. If the app finds and loads a different version of the DLL, that version won’t contain your breakpoints. Другие методы отладки библиотек DLL см. в разделе Отладка проектов DLL. For other methods of debugging DLLs, see Debugging DLL projects.

Если управляемое приложение вызывает собственную библиотеку DLL или ваше собственное приложение вызывает управляемую библиотеку DLL, можно выполнить отладку библиотеки DLL и вызывающего приложения. If your managed app calls a native DLL, or your native app calls a managed DLL, you can debug both the DLL and the calling app. Дополнительные сведения см. в разделе Практическое руководство. Отладка в смешанном режиме. For more information, see How to: Debug in mixed mode.

Собственные и управляемые DLL-проекты имеют разные параметры для указания вызывающих приложений. Native and managed DLL projects have different settings to specify calling apps.

Указание вызывающего приложения в собственном проекте DLL Specify a calling app in a native DLL project

Выберите проект C++ DLL в обозревателе решений. Select the C++ DLL project in Solution Explorer. Щелкните значок Свойства, нажмите сочетание клавиш ALT+ВВОД или щелкните проект правой кнопкой мыши и выберите элемент Свойства. Select the Properties icon, press Alt+Enter, or right-click and choose Properties.

В диалоговом окне

Страницы свойств убедитесь, что в поле Настройка в верхней части окна задано значение Отладка. In the

Property Pages dialog box, make sure the Configuration field at the top of the window is set to Debug.

Выберите Свойства конфигурации > Отладка. Select Configuration Properties > Debugging.

В списке Запускаемый отладчик выберите Локальный отладчик Windows или Удаленный отладчик Windows. In the Debugger to launch list, choose either Local Windows Debugger or Remote Windows Debugger.

В поле Команда или Удаленная команда добавьте полный путь и имя файла вызывающего приложения, например EXE-файл. In the Command or Remote Command box, add the fully qualified path and filename of the calling app, such as an .exe file.

Добавьте необходимые аргументы программы в поле Аргументы команды. Add any necessary program arguments to the Command Arguments box.

Нажмите кнопку ОК. Select OK.

Указание вызывающего приложения в управляемом проекте DLL Specify a calling app in a managed DLL project

Выберите проект C# или Visual Basic DLL в обозревателе решений. Select the C# or Visual Basic DLL project in Solution Explorer. Щелкните значок Свойства, нажмите сочетание клавиш ALT+ВВОД или щелкните проект правой кнопкой мыши и выберите элемент Свойства. Select the Properties icon, press Alt+Enter, or right-click and choose Properties.

Убедитесь, что в поле Настройка в верхней части окна задано значение Отладка. Make sure that the Configuration field at the top of the window is set to Debug.

В разделе Запустить действие Under Start action:

Для библиотек DLL .NET Framework выберите Запустить внешнюю программу и добавьте полный путь и имя вызывающего приложения. For .NET Framework DLLs, select Start external program, and add the fully qualified path and name of the calling app.

Или выберите Запустить браузер с URL-адресом и введите URL-адрес локального приложения ASP.NET. Or, select Start browser with URL and fill in the URL of a local ASP.NET app.

Для библиотек DLL .NET Core страница свойств Отладка отличается. For .NET Core DLLs, the Debug Properties page is different. Выберите Исполняемый файл из раскрывающегося списка Запуск, а затем добавьте полный путь и имя вызывающего приложения в поле Исполняемый файл. Select Executable from the Launch dropdown, and then add the fully qualified path and name of the calling app in the Executable field.

Добавьте необходимые аргументы командной строки в поле Аргументы командной строки или Аргументы приложения. Add any necessary command-line arguments in the Command line arguments or Application arguments field.

Используйте Файл > Сохранить выбранные элементы или CTRL+S, чтобы сохранить изменения. Use File > Save Selected Items or Ctrl+S to save changes.

Отладка из проекта DLL Debug from the DLL project

Задайте точки останова в проекте DLL. Set breakpoints in the DLL project.

Щелкните проект DLL правой кнопкой мыши и выберите Назначить запускаемым проектом. Right-click the DLL project and choose Set as Startup Project.

Убедитесь, что в поле Конфигурация решений установлено значение Отладка. Make sure the Solutions Configuration field is set to Debug. Нажмите клавишу F5, щелкните зеленую стрелку Запуск или выберите Отладка > Начать отладку. Press F5, click the green Start arrow, or select Debug > Start Debugging.

Дополнительные советы. Additional tips:

Если отладка не достигает точек останова, убедитесь, что выходные данные библиотеки DLL (по умолчанию — папка

\Debug) — это расположение, которое вызывает вызывающее приложение. If debugging does not hit your breakpoints, make sure that your DLL output (by default, the

\Debug folder) is the location that the calling app is calling.

Если вы хотите прервать выполнение кода в управляемом вызывающем приложении из собственной библиотеки DLL или наоборот, включите отладку в смешанном режиме. If you want to break into code in a managed calling app from a native DLL, or vice versa, enable mixed mode debugging.

В некоторых сценариях может потребоваться сообщить отладчику, где найти исходный код. In some scenarios, you may need to tell the debugger where to find the source code. Дополнительные сведения см. в разделе Использование страниц «Символы не загружены» или «Нет исходного кода». For more information, see Use the No Symbols Loaded/No Source Loaded pages.

Читайте также:  Нет резервирования обновления до windows 10
Оцените статью