符号及其可见性标记
在共享库中的符号(Symbols)表示源代码中的函数、变量和类,符号需要输出才能被客户端使用。共享库的客户端指的是应用或者其他库,我们可以将源代码中所有的符号都输出,也可以通过选择哪些符号是可见的(publicly visible),哪些符号是隐藏(invisible )的,我们需要一种特殊的手段来指明符号的这种属性。这些可见的我们称为公有符号(public symsbols),这个过程称之为导出(export)或让符号公有可见(public visible),除了这些公有符号外的其他符号对于外部不可见。
大多数平台的编译器默认都会将符号隐藏,少部分平台的编译器需要借助编译选项来隐藏它们。当我们编译一个共享库时,变量必须被标记成导出;当使用一个共享库某些编译器可能还需要导入(import)标记声明。
取决于你的目标平台,QT提供了一些特殊的宏包含必要的导入导出定义:
- Q_DECL_EXPORT 编译动态库在声明处加入该宏表示导出
- Q_DECL_IMPORT 编译客户端在声明处加入该宏表示导入
这样一来我们需要保证正确的宏在恰当的时候被调用,而无须关心我们是在编译动态库或者是使用动态库了。通常,我们会增加一个特殊的头文件来解决这个问题。我们先假设我们的动态库名字叫做mysharedlib
,一个特殊的头文件mysharedlib_global.h
将会被创建,其内容大概是这样:
#include <QtCore/QtGlobal>
#if defined(MYSHAREDLIB_LIBRARY)
# define MYSHAREDLIB_EXPORT Q_DECL_EXPORT
#else
# define MYSHAREDLIB_EXPORT Q_DECL_IMPORT
#endif
在pro文件中我们增加一个定义来表明这是一个共享库编译:
DEFINES += MYSHAREDLIB_LIBRARY
在每一个库的头文件将会包含这个特殊的头文件,像是这样:
#include "mysharedlib_global.h"
MYSHAREDLIB_EXPORT void foo();
class MYSHAREDLIB_EXPORT MyClass...
这样一来我们能够保证Q_DECL_EXPORT
、Q_DECL_IMPORT
的正确使用,你可以在Qt的源文件看到这个方法的应用。
头文件依赖问题
通常情况下,客户端只包含共享库的公共头文件。编译静态库时有可能引用了其他第三方库,第三方库的位置可能回随着部署而出现位置不正确、找不到的情况。移除编译动态库时使用到的内置头文件非常有必要。
举个例子:
静态库可能会export一个与硬件相关的句柄,这通常是由第三方提供的。
#include <footronics/device.h>
class MyDevice {
private:
FOOTRONICS_DEVICE_HANDLE handle;
};
在你使用这个动态库时,可能出现找不到动态库的情况。
同样的情况也出现在Qt Disigner中,当我们使用多重继承(multiple inheritance)或者聚合(aggregation)时:
#include "ui_widget.h"
class MyWidget : public QWidget {
private:
Ui::MyWidget m_ui;
};
当我们部署一个库完成后,使用它可能会出现footronics/device.h
或ui_widget.h找不到内置头文件情况,可以通过使用各种C++编程书籍中描述的实现习语指针来避免。对于具有值语义的类,请考虑使用QSharedDataPointer。
二进制兼容性
对于加载共享库的客户机,要正常工作,所用类的内存布局必须与用于编译客户机的库版本的内存布局完全匹配。换句话说,客户机在运行时找到的库必须与编译时使用的版本二进制兼容。
如果客户机是一个独立的软件包,提供了它所需的所有库,那么这通常不是问题。
但是,如果客户机应用程序依赖于属于不同安装包或操作系统的共享库,那么我们需要考虑共享库的版本控制方案,并决定在哪个级别维护二进制兼容性。例如,相同主要版本号的Qt库保证是二进制兼容的。
保持二进制兼容性会对可以对类进行的更改施加一些限制。在C++中使用KDE策略/二进制兼容性问题可以找到一个很好的解释。这些问题应该从动态库设计的一开始就加以考虑。我们建议尽可能使用信息隐藏原理和指针实现技术。
[1] QT帮助文档