Создание динамических библиотекВступлениеТема динамических библиотек, казалось бы, раскрыта 42 главе фундаментального труда Шлее. Однако, при ближайшем рассмотрении выясняется, что раскрыта она, прямо скажем, недостаточно. В приведенном примере Шлее ограничивается рассмотрением создания и использования DLL, содержащей только функции, но не классы. После чего сразу переходит к рассмотрению плагинов.
Между тем, при создании динамических библиотек, содержащих классы, возникают некоторые тонкости, которые необходимо учитывать. Об этом и пойдет речь в данном howto.
При описании я буду предполагать, что читатель в общих чертах знаком с основами Qt, и у него не возникнет вопросов типа
«а что такое QVBoxLayout?». Так что я не буду описывать текст исходников примера (он сам по себе весьма прост), а сосредоточусь непосредственно на особенностях, характерных для DLL. Если с пониманием примера все же возникнут трудности — рекомендую сперва прочитать того же Шлее, а уже потом обратится к этому тексту.
И еще — здесь рассматривается только явная компоновка приложения и библиотеки. Если необходимо подгрузить класс динамически — лучше оформить его как плагин. В противном случае вас ждут большие проблемы...
1.БиблиотекаЛежит в директории
src/dll.
Библиотека содержит простейший виджет, производный от
QLabel. Им можно пользоваться точно также, как любым другим виджетом Qt: добавлять в свои виджеты, использовать слоты/сигналы, создавать производные классы и т.д.
Отличие от обычного Qt приложения сосредоточены, в основном, в двух файлах:
ddll.h и
dll.pro.
ddll.h:C++ (Qt)
#ifndef D_DLL
#define D_DLL
#include <QtGlobal>
#ifdef D_SHARED_LIB
#define D_SHARED Q_DECL_EXPORT
#else
#define D_SHARED Q_DECL_IMPORT
#endif
#endif
Вообще говоря, без этого файла можно обойтись. Если пользоваться нормальным компилятором. Но, помимо нормальных компиляторов, существуют еще компиляторы от microsoft. И в них, по слухам, требуется подобное извращение.
Что здесь написано.
Q_DECL_EXPORT и
Q_DECL_IMPORT — это два недокументированных (пока?) макроса, которые введены разработчиками Qt специально, чтобы обойти эти грабли в MSVS. Первый из них требуется писать перед объявлением класса в DLL, второй — в приложениях, которые этот DLL используют. Во всех остальных компиляторах они равны пустой строке.
Соответственно, перед всеми классами в нашей библиотеке мы должны проставить наш макрос
D_SHARED:
C++ (Qt)
#include <QLabel>
#include "ddll.h"
class D_SHARED DLabel : public QLabel
{
Q_OBJECT
....
На этом отличия исходного кода кончаются. Главные различия касаются файла проекта:
dll.pro:
TEMPLATE = lib
CONFIG += shared
# in
VERSION = 1.0.0
DEFINES += D_SHARED_LIB
SOURCES = dlabel.cpp
HEADERS = dlabel.h ddll.h
# out
TARGET = dlabel
DLLDESTDIR = ../../bin
DESTDIR = ../../lib
Полагаю,
TEMPLATE и
CONFIG в комментариях не нуждаются. На некоторых других вещах стоит остановится подробнее.
DEFINES — определяем макрос
D_SHARED_LIB, который используется в
ddll.h (см. выше).
DESTDIR и
DLLDESTDIR — мы помещаем получившуюся библиотеку в
lib. Но само приложение у нас будет находится в
bin, поэтому мы копируем DLL туда.
VERSION — а вот здесь нам необходимо небольшое лирическое отступление.
Существует такая вещь, как бинарная совместимость. Если кратко — мы создаем некоторую структуру, помещаем ее в DLL. Потом наши приложения этот DLL юзают. А потом мы ее изменяем. И перезаписываем DLL поверх старого. А приложения обращаются к ней по старым адресам... Результат, думаю, ясен.
Так вот, это называется — отсутствие бинарной совместимости. В просторечии — dll hell.
И чтобы хоть частично этого избежать, вводят понятие версии DLL. Предполагается, что DLL бинарно совместимы, если старший номер версии у них не отличается. Ответственность за реализацию такого поведения целиком лежит на разработчике библиотеки.
Однако, вернемся к нашим баранам. Увидев
VERSION,
qmake во-первых, принимает необходимые меры, чтобы эта версия прописалась вo внутренних свойствах DLL, а, во-вторых — приписывает ее старший номер к имени создаваемого DLL. Т.е. в нашем примере это будет
dlabel1.dll. Соответственно, если мы изменим интерфейс нашей библиотеки до полной несовместимости, для предотвращения использования ее старыми приложениями мы должны будем просто сменить
VERSION на
2.0.0. И тогда новые версии будут использовать
dlabel2.dll, старые —
dlabel1.dll.
2.ПриложениеЛежит в директории
src/main.
Как я уже писал выше — мы можем использовать наш виджет точно также, как любой другой. Единственные отличия — в
pro-файле.
main.pro:
TEMPLATE = app
CONFIG += qt warn_on
#in
DEPENDPATH += ../dll
INCLUDEPATH += ../dll
LIBS += -L../../lib -ldlabel
HEADERS = dlcat.h
SOURCES = dlcat.cpp dmain.cpp
RESOURCES = longcat.qrc
#out
DESTDIR = ../../bin
TARGET = longcat
Остановимся на этих отличиях подробнее.
INCLUDEPATH — указываем путь к хидерам библиотеки.
DEPENDPATH — объясняем
qmake, что хидеры библиотеки необходимо учитывать при построении зависимостей.
LIBS — подключаем библиотеку. При этом путь и сам библиотека указывается отдельно и в самом общем виде (без расширений и т.п.). В такой форме это будет правильно обработано для любого компилятора.
VERSION у библиотеки должен быть указан, иначе это не сработает. При наличии нескольких версий будет выбрана последняя.
3. Возможные граблиПри использовании динамической линковки следует помнить о некоторых общих вещах, которые необходимо учитывать во избежание ошибок. Впрочем, при нормальной работе Qt, в большинстве случаев, сама заботится о том, чтобы сделать «нарушение правил» достаточно сложным...
Итак:
1. Пользуйтесь для сборки библиотеки и приложения одним и тем же компилятором.Разные компиляторы по-разному кодируют в объектном коде имена функций, классов и т.д. Более того, могут отличаться и смещения полей внутри класса. По этому попытка подключить dll, собранный в другом компиляторе, скорее всего, увенчается успехом лишь в том случае, если все ф-ции отмечены как
extern "C" и смещения структур выравнены принудительно. Для классов это, разумеется, не подходит.
Как ни трудно понять, при самостоятельной разработке библиотеки эта проблема вряд ли возникнет. А вот если мы имеем чужой dll и закрытый код... Тем, кто попал в такую ситуацию, остается только посочувствовать.
2. ...И одними и теми же опциями командной строки — в части, влияющей на свойства генерируемого кода.Имеются в виду, прежде всего, опции, включающие поддержку исключений, RTTI, управляющие отладочной информацией и т. п. Как ни трудно догадаться, если в один модуль скомпилирован с поддержкой RTTI, другой — без, то при использовании этого самого RTTI программу ждут большие неприятности. То же касается и отладки (там разные версии стандартных библиотек — см. ниже).
В Qt об опциях командной строки компилятора заботится qmake, так что в обычной ситуации эта проблема возникать не должна. Иными словами — если не делать явных глупостей, вроде смешения
debug и
release в одну кучу, то этого и не произойдет.
3. Используя низкоуровневые ресурсы, обеспечте инкапсуляцию всей фактической работы с ними в рамках одного модуля, линкуемого динамически.Если
CRT скомпонована статически, то ресурс (например — память), выделенный в рамках одного модуля, невозможно корректно освободить в рамках другого: каждый из них будет оперировать со своим адресным пространством и своими копиями внутренних структур, обеспечивающих нормальный доступ к ресурсам. Кроме того, разумеется, нужно следить, чтобы во всех модулях использовалась CRT одной и той же версии.
В Qt все низкоуровневые вызовы инкапсулированы в рамках модуля
QtCore. Так что помнить это правило нужно лишь при низкоуровневой работе с вещами, для которых Qt не предоставляет удовлетворительной поддержки. Впрочем, такую работу нужно инкапсулировать в любом случае. В штатных же случаях — просто пользуйтесь стандартными средствами библиотеки — и будет вам счастье
.
Разумеется, при этом
саму Qt необходимо линковать динамически. Вообще,
динамическую и статическую линковку в одну кучу мешать не следует — многих проблем удастся избежать.
И еще, касательно данного примера. Следует иметь в виду, что он писался в предположении, что библиотека будет линковаться
только динамически. Для для случая статической линковки макросы
Q_DECL_IMPORT/EXPORT следует убрать. Как средствами qmake отличить статическую линковку от динамической и обеспечить включение/выключение нужных макросов — продемонстрировано
ниже по треду в письме от
pastor, на модификации моего примера. За что ему отдельное спасибо.
Собственно, на этом всё. Enjoy!
Проверено
- под Windows 2003 в Qt 4.4.1, в компиляторе mingw, gcc version 3.4.5
- под Windows XP x64 в Qt 4.4.3, в MSVS 2008
- под openSuse 11 в Qt 4.4.3, в gcc version 4.3.1 20080507
За последние 2 варианта — спасибо
pastor!