在 CMakeLists.txt 中添加編譯動態(tài)庫選項
在使用 CMake 構(gòu)建項目時牡属,一個常見的應(yīng)用就是使用 CMake 編譯一個庫文件了。而編譯成一個動態(tài)庫或者靜態(tài)庫又是編譯庫文件時經(jīng)常使用的一個選項驼鹅。本文介紹了如何在 CMake 中添加一個選項來控制是否將庫編譯為動態(tài)庫咐容,且該選項可以和 CMake 一樣跨平臺使用。
本文并不從動態(tài)庫和靜態(tài)庫的起源開始講起,因此有些預(yù)備知識需要你提前了解米奸。這些知識不需要你深刻的了解并實踐過,只需看過相關(guān)文章有所了解即可爽篷。這些預(yù)備知識包括:
C++ 基本編程知識悴晰。
什么是動態(tài)庫。
動態(tài)庫和靜態(tài)庫的區(qū)別逐工。
CMake 是干什么的铡溪,CMake 的基本知識。
上面的預(yù)備知識可以在網(wǎng)上很容易查閱得到泪喊,Static vs Dynamic Libraries 這篇英文文章介紹的也比較詳細(xì)棕硫,可以作為參考。
主要難點
關(guān)于在 CMake 中添加動態(tài)庫的選項袒啼,主要的難點就在于 windows 平臺上的改動較大哈扮。在 linux 平臺下纬纪,動態(tài)庫和靜態(tài)庫的源代碼是完全一樣的,只需要修改編譯參數(shù)即可滑肉。而 windows 平臺下包各,如果想把靜態(tài)庫變成動態(tài)庫的話,所有的源碼都需要改變靶庙!因此如果你之前寫了大量代碼而沒有考慮到 windows 平臺上編譯動態(tài)庫的話问畅,修改起來可能會是一個很大的工程。本文將以一個簡單的實例來介紹如何編寫一個可跨平臺的帶動靜態(tài)編譯參數(shù)的 CMake 項目六荒。示例代碼可以在 這里 查看护姆。
Windows 平臺下的動態(tài)庫導(dǎo)入導(dǎo)出
前面我們提到過,在 Windows 平臺中生成動態(tài)庫其源碼和靜態(tài)庫是不同的掏击。在 Windows 平臺中签则,我們導(dǎo)出動態(tài)庫時,除了會生成 .dll
動態(tài)庫之外還會生成一個 .lib
文件铐料。這個 .lib
文件和靜態(tài)庫的 .lib
文件不同,它里面并不保存代碼生成的二進(jìn)制文件豺旬,而是所有需要導(dǎo)出符號的符號表钠惩。因此這個 .lib
文件和編譯靜態(tài)庫生成的 .lib
文件相比會小很多。而這個導(dǎo)出的符號表是需要我們在源碼中進(jìn)行指定的族阅。如果我們希望將將一個符號(symbol)導(dǎo)出(這里的符號可以指類篓跛、函數(shù)等各種類型),需要在其前面加上 __declspec(dllexport)
標(biāo)志坦刀。這樣這個符號的相關(guān)信息就會導(dǎo)出的 .lib
中的符號表中了愧沟。如果我們的源碼中沒有任何 __declspec(dllexport)
的話,我們依然可以成功的編譯出動態(tài)庫鲤遥,但是并不會生成保存符號表的.lib
文件沐寺。這也是在 Windows 平臺下編譯動態(tài)庫經(jīng)常出現(xiàn)的問題,如果我們的源碼是在 Linux 平臺下編寫的話盖奈,更是很容易忘記修改源碼混坞。以下是一個導(dǎo)出 MyClass
的例子:
class __declspec(dllexport) MyClass {
public:
static void MyPrint();
};
除了導(dǎo)出符號標(biāo)識符 __declspec(dllexport)
以外,我們作為用戶使用動態(tài)庫的時候钢坦,對應(yīng)頭文件中的符號還需要有 __declspec(dllimport)
標(biāo)識符來表示這個符號是從動態(tài)庫導(dǎo)入的究孕。對應(yīng)上面的 MyClass
這個例子,我們包含的頭文件應(yīng)該有以下內(nèi)容:
class __declspec(dllimport) MyClass {
public:
static void MyPrint();
};
一般對于一個庫文件我們并不想對導(dǎo)入和導(dǎo)出分別寫兩個幾乎同樣的頭文件爹凹,因此往往使用宏來替代直接使用 __declspec(dllexport)
和 __declspec(dllimport)
關(guān)鍵字厨诸。即:
#pragma once
#ifdef MY_LIB_EXPORTS
#define MY_LIB_API __declspec(dllexport)
#else
#define MY_LIB_API __declspec(dllimport)
#endif
class MY_LIB_API MyClass {
public:
static void MyPrint();
};
這樣我們只需要在編譯(導(dǎo)出)這個庫的時候,給編譯器添加 MY_LIB_EXPORTS
宏禾酱。而在使用該庫的時候什么都不定義即可微酬。
編寫 export 頭文件
了解了 windows 平臺下頭文件的導(dǎo)出規(guī)則之后绘趋,現(xiàn)在我們來編寫一個名為 my_lib_export.h 的頭文件來專門的控制 MY_LIB_API
宏的定義。注意到上面的討論只是在 Windows 平臺以及編寫動態(tài)庫時才會發(fā)生得封,對應(yīng) Linux 平臺以及編寫靜態(tài)庫時埋心,我們按照教科書上的定義按部就班的寫我們的 C++ 代碼即可。為了使 MY_LIB_API
的定義同時考慮到 Linux 平臺以及靜態(tài)庫的的情況忙上。這里豐富 MY_LIB_API
的定義如下(my_lib_export.h 中的內(nèi)容):
#pragma once
#ifdef MY_LIB_SHARED_BUILD
#ifdef _WIN32
#ifdef MY_LIB_EXPORTS
#define MY_LIB_API __declspec(dllexport)
#else
#define MY_LIB_API __declspec(dllimport)
#endif // MY_LIB_EXPORTS
#else
#define MY_LIB_API
#endif // _WIN32
#else
#define MY_LIB_API
#endif // MY_LIB_SHARED_BUILD
這里除了使用 MY_LIB_EXPORTS
宏來判斷是否為導(dǎo)出動態(tài)庫以外拷呆,還使用到了編譯器自帶的 _WIN32
宏來判斷是否是在 windows 平臺上以及使用了需要我們自己定義的另外一個宏 MY_LIB_SHARED_BUILD
來判斷是否正在編譯動態(tài)庫。除了上一節(jié)討論的情況以外疫粥,我們均將 MY_LIB_API
的值設(shè)置為了空茬斧,即什么都沒有定義。此時和普通的類定義完全相同梗逮。有了這個頭文件之后项秉,我們只需要在導(dǎo)出符號表的頭文件中包含該頭文件,就可以使用 MY_LIB_API
宏了慷彤。
關(guān)于 MY_LIB_SHARED_BUILD
和 MY_LIB_EXPORTS
宏的定義娄蔼,我將在下面 CMakeLists.txt 的編寫一節(jié)進(jìn)行介紹。最后在多介紹一點底哗,事實上 my_lib_export.h 這個頭文件是可以通過 CMake 提供的 GenerateExportHeader 命令自動生成的岁诉。考慮到自動生成的額外配置以及生成后的文件多出的無關(guān)內(nèi)容不易于理解跋选,這里不對該命令的使用進(jìn)行介紹了涕癣,如果有興趣的話,可以自行了解前标。以下內(nèi)容摘抄自一個自動生成的 export 頭文件坠韩,可以看出其表達(dá)的內(nèi)容和我們上面自行定義的基本相同:
#ifndef LOGGING_EXPORT_H
#define LOGGING_EXPORT_H
#ifdef LOGGING_STATIC_DEFINE
# define LOGGING_EXPORT
# define LOGGING_NO_EXPORT
#else
# ifndef LOGGING_EXPORT
# ifdef logging_EXPORTS
/* We are building this library */
# define LOGGING_EXPORT __declspec(dllexport)
# else
/* We are using this library */
# define LOGGING_EXPORT __declspec(dllimport)
# endif
# endif
# ifndef LOGGING_NO_EXPORT
# define LOGGING_NO_EXPORT
# endif
#endif
#ifndef LOGGING_DEPRECATED
# define LOGGING_DEPRECATED __declspec(deprecated)
#endif
#ifndef LOGGING_DEPRECATED_EXPORT
# define LOGGING_DEPRECATED_EXPORT LOGGING_EXPORT LOGGING_DEPRECATED
#endif
#ifndef LOGGING_DEPRECATED_NO_EXPORT
# define LOGGING_DEPRECATED_NO_EXPORT LOGGING_NO_EXPORT LOGGING_DEPRECATED
#endif
#if 0 /* DEFINE_NO_DEPRECATED */
# ifndef LOGGING_NO_DEPRECATED
# define LOGGING_NO_DEPRECATED
# endif
#endif
#endif /* LOGGING_EXPORT_H */
編寫 CMakeLists 文件
上面的內(nèi)容介紹了如何修改源碼:
編寫 my_lib_export.h 頭文件在其中定義
MY_LIB_API
宏。在需要導(dǎo)出的符號前炼列,加上
MY_LIB_API
宏只搁。
接下來將介紹如何編寫 CMakeLists.txt 文件,其實只需要在 CMakeLists.txt 文件中做兩件事:
添加是否編譯動態(tài)庫選項俭尖。
定義相關(guān)的宏幫助源碼“理解”應(yīng)該如何定義
MY_LIB_API
宏须蜗。
首先頂層 CMakeLists.txt 文件中需要添加如下語句來添加編譯選項:
option(BUILD_SHARED_LIBS "Specifies the type of libraries (SHARED or STATIC) to build" OFF)
其次在編譯庫的 CMakeLists.txt 文件中需要根據(jù)指定的編譯選項,來定義不同的編譯形式以及宏定義:
if (BUILD_SHARED_LIBS)
add_library(my_lib SHARED ${SrcFiles})
target_compile_definitions(my_lib PUBLIC -DMY_LIB_SHARED_BUILD)
target_compile_definitions(my_lib PRIVATE -DMY_LIB_EXPORTS)
else()
add_library(my_lib STATIC ${SrcFiles})
endif()
這里在編譯動態(tài)庫的時候會添加兩個宏定義 MY_LIB_SHARED_BUILD
以及 MY_LIB_EXPORTS
目溉。注意到這里 MY_LIB_EXPORTS
宏定義的訪問符為 PRIVATE
即這個宏定義只是在編譯時有效明肮。而 MY_LIB_SHARED_BUILD
宏定義的訪問符為 PUBLIC
,即無論是編譯還是安裝后作為庫文件引用時均有效缭付。這樣我們就可以保證源碼中 MY_LIB_API
宏被正確定義了柿估。
完整的 CMakeLists.txt 文件和源碼可以在 github 倉庫 中查看。
參考資料: