ilyachalov (ilyachalov) wrote,
ilyachalov
ilyachalov

Category:

Создание библиотеки классов (.dll): неявное связывание

Начало тут:
1. библиотека классов;
2. создание библиотеки классов (в тексте);
3. создание библиотеки классов (объектный файл);
4. создание библиотеки классов (.lib);
5. создание библиотеки классов (.dll): теория;
6. создание библиотеки классов (.dll): проект DLL;
7. создание библиотеки классов (.dll): экспорт классов.

Область применения: язык программирования C++, операционная система «Windows 7», среда разработки «Visual Studio Community 2017».

Близкая по теме англоязычная статья на сайте «Microsoft Docs»: «Walkthrough: Create and use your own Dynamic Link Library (C++)» (по-русски «Пошаговое руководство: создание и использование DLL (C++)»).

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

Создание проекта прикладной программы

1. Создадим пустой проект с названием MyApp. Итак, пункт меню «Файл – Создать – Проект...». В открывшемся окне «Создание проекта» в меню слева выберем пункт «Установленные – Visual C++». В меню в центре выберем пункт «Пустой проект». В этом же окне внизу в реквизите «Имя» вместо названия проекта по умолчанию указываем MyApp. (Реквизит «Расположение» оставим по умолчанию, галки «Создать каталог для решения» и «Добавить в систему управления версиями» я снял.) Запускаем создание проекта кнопкой «OK» в правом нижнем углу окна.

2. Наша прикладная программа — это та же программа, которую я использовал для тестирования статической библиотеки классов в предыдущих постах. То есть добавляем в проект файл test_app.cpp. Для этого копируем этот файл в папку проекта MyApp (эта папка, напомню, находится по пути Расположение\Имя\, составленном из упомянутых в первом пункте реквизитов «Расположение» и «Имя»). После этого я добавляю этот файл в проект через «Обозреватель решений» в ветку «Исходные файлы» нашего проекта.

Включение файлов динамической библиотеки в прикладную программу

3. Напомню, что в файл прикладной программы test_app.cpp включен интерфейс библиотеки классов в виде заголовочного файла mylib.h:
#include "mylib.h" // интерфейс библиотеки классов
Поэтому копируем заголовочный файл mylib.h из проекта нашей динамической библиотеки ProjectDLL в проект нашей прикладной программы. То есть копирую этот файл в папку проекта нашей прикладной программы и включаю в проект через «Обозреватель решений» в ветку «Файлы заголовков».

4. Уберем из заголовочного файла mylib.h нашего проекта прикладной программы MyApp модификаторы __declspec(dllexport), ведь в прикладной программе они не нужны, тут требуется не экспорт, а импорт классов.

5. Как наша прикладная программа узнает, какие программные сущности нужно импортировать и откуда их импортировать? В данном посте речь идет про неявное связывание DLL и прикладной программы. Этот метод связывания подразумевает передачу компоновщику, входящему в состав компилятора, нужной для связывания информации (само связывание будет выполнено компоновщиком операционной системы на этапе загрузки прикладной программы (load time) в оперативную память). Такая передача происходит через файл библиотеки импорта .lib. Этот файл был создан компилятором при сборке проекта нашей DLL и находится в той же папке проекта DLL, что и файл самой DLL. (Я не упомянул о нем в посте о создании проекта DLL, потому что в тот момент в этом не было нужды.)

Тут нужно отметить, что, несмотря на совпадение расширений (.lib) у статической библиотеки классов, про которую шла речь в предыдущих постах этой серии, и у библиотеки импорта, это совершенно разные файлы с совершенно разным содержимым. Это можно понять даже по разнице их размеров. Например, размер статической библиотеки, созданной в одном из предыдущих постов, составил 984 Кб, а размер библиотеки импорта, созданной в проекте по созданию нашей DLL, составил 8 Кб.

Итак, копируем файл библиотеки импорта ProjectDLL.lib из папки Расположение\ProjectDLL\Release\ проекта по созданию DLL в папку Расположение\MyApp\ проекта нашей прикладной программы. Далее подключаем эту библиотеку импорта через свойства проекта. Для этого сначала на панели инструментов среды делаем активной конфигурацию решения Release и платформу решения x86. Теперь откроем окно свойств проекта. Я это делаю через «Обозреватель решений», щелкнув правой кнопкой мыши по названию нашего проекта. В открывшемся контекстном меню следует выбрать самый последний пункт «Свойства». В открывшемся окне «Страницы свойств MyApp» сначала следует сверху в реквизитах «Конфигурация» и «Платформа» проверить, что выбраны нужные нам конфигурация решения «Release» и платформа решения «x86» (она же — «Win32») (свойства проекта можно устанавливать разными для разных конфигураций и платформ решения).

После этого в меню слева выберем пункт «Свойства конфигурации – Компоновщик – Ввод». Далее в списке свойств справа в значение свойства «Дополнительные зависимости» следует добавить имя нашей библиотеки импорта ProjectDLL.lib. Сохраним значения свойств проекта с помощью кнопки «OK» в правом нижнем углу окна.

Готово

Теперь уже можно запустить компиляцию (сборку) проекта MyApp нашей прикладной программы с помощью пункта меню «Сборка – Собрать решение». В результате сборки будет создана папка Расположение\MyApp\Release\, в которой появится исполняемый файл нашей прикладной программы MyApp.exe.

Однако, если запустить этот файл на выполнение, получим следующее сообщение операционной системы об ошибке: «Запуск программы невозможен, так как на компьютере отсутствует ProjectDLL.dll. Попробуйте переустановить программу.».



Это логично. Вспомним порядок неявного связывания. При загрузке (load time) исполняемого файла в оперативную память компоновщик операционной системы пытается выполнить связывание нашей прикладной программы с динамической библиотекой ProjectDLL.dll. Для этого этот компоновщик запускает поиск файла динамической библиотеки ProjectDLL.dll. Порядок этого поиска может варьироваться в зависимости от разнообразных факторов. Об этом можно почитать в большой англоязычной статье «Dynamic-Link Library Search Order» на сайте «Microsoft Docs» (я недавно делал ее перевод на русский).

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

Зато, к примеру, каталог, из которого запускается исполняемый файл нашей прикладной программы, входит в вышеупомянутый поисковый список каталогов (папок). Поэтому я копирую нашу DLL ProjectDLL.dll из каталога Расположение\ProjectDLL\Release\ в каталог Расположение\MyApp\Release\ и снова запускаю исполняемый файл нашей прикладной программы MyApp.exe.

Теперь наша прикладная программа отрабатывает без ошибок:



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

Но тут нужно помнить, что для 32-разрядных прикладных программ системный каталог в 32-разрядной версии операционной системы «Windows» — это каталог ..\Windows\System32\, а для 64-разрядной версии операционной системы «Windows» — это каталог ..\Windows\SysWOW64\. У меня как раз второй случай, 64-разрядная версия операционной системы «Windows 7» (папка ..\Windows\System32\ тоже присутствует, но помещенную туда 32-разрядную версию нашей DLL операционная система не находит).

Оптимизация интерфейса DLL

Теперь перейдем к обещанным в начале поста изменениям в заголовочный файл (интерфейс DLL) mylib.h.

Как мы увидели, помечать импортируемые в нашу прикладную программу классы в качестве «импортируемых» необязательно. При сборке прикладной программы компоновщик, ориентируясь на информацию из библиотеки импорта, прекрасно понимает, какие классы являются импортируемыми. Однако, помечать импортируемые классы в качестве «импортируемых» в исходном коде всё-таки рекомендуют. Это упростит и убыстрит работу компоновщика. Такая пометка классов выполняется с помощью модификатора __declspec(dllimport) точно так же, как при экспорте классов при создании DLL применяется модификатор __declspec(dllexport).

Получается, что при сборке DLL в заголовочном файле mylib.h в экспортируемых классах должны стоять модификаторы экспорта, а при сборке прикладной программы в том же заголовочном файле в тех же классах (только теперь импортируемых) должны стоять модификаторы импорта. То есть разработчик DLL должен иметь две версии этого заголовочного файла — одну для сборки DLL, а другую — для передачи (продажи) разработчику прикладной программы. Естественно, что это неудобно.

Для обхода этой проблемы разработчик DLL может вставить в начало заголовочного файла mylib.h следующую конструкцию:
#ifdef ProjectDLL_EXPORTING
    #define CLASS_DECLSPEC __declspec(dllexport)
#else
    #define CLASS_DECLSPEC __declspec(dllimport)
#endif
а при пометке классов модификатором теперь уже можно использовать макрос CLASS_DECLSPEC:
...
class CLASS_DECLSPEC book
...
class CLASS_DECLSPEC buyer
...

Теперь, если в проекте существует символ-макрос ProjectDLL_EXPORTING, то классы будут помечены модификатором экспорта. Если же символ-макрос ProjectDLL_EXPORTING не определен, то классы будут помечены модификатором импорта. В проекте по созданию DLL разработчик DLL определяет указанный символ-макрос, а в проекте по созданию прикладной программы разработчик прикладной программы этот символ-макрос не определяет. Таким образом разработчик DLL имеет только одну версию заголовочного файла mylib.h — и для разработки DLL, и для передачи (продажи) разработчику прикладной программы.

При использовании в среде «Visual Studio Community 2017» специализированного шаблона для создания проекта по созданию DLL среда сама вставляет вышеописанную конструкцию в заголовочный файл и сама определяет символ-макрос с именем ИмяПроекта_EXPORTING. При использовании для создания DLL пустого проекта (как это делал я в этой серии постов) разработчик DLL может сам вставить вышеуказанную конструкцию в заголовочный файл и сам определить соответствующий символ-макрос.

Я определил символ-макрос ProjectDLL_EXPORTING в свойствах проекта. Открываем свойства проекта DLL для соответствующей конфигурации (конфигурация решения «Release», платформа решения «x86»). Выбираем в левом меню пункт «Свойства конфигурации – С/С++ – Препроцессор». Далее, справа, в списке свойств выбираем свойство «Определения препроцессора» и добавляем в значение этого свойства символ-макрос ProjectDLL_EXPORTING. Сохраняем свойства проекта с помощью кнопки «OK» в правом нижнем углу окна свойств.
Tags: Образование, Программирование
Subscribe

  • Post a new comment

    Error

    Anonymous comments are disabled in this journal

    default userpic

    Your IP address will be recorded 

  • 0 comments