在 CMakeLists.txt 中添加編譯動(dòng)態(tài)庫(kù)選項(xiàng)
在使用 CMake 構(gòu)建項(xiàng)目時(shí),一個(gè)常見(jiàn)的應(yīng)用就是使用 CMake 編譯一個(gè)庫(kù)文件了。而編譯成一個(gè)動(dòng)態(tài)庫(kù)或者靜態(tài)庫(kù)又是編譯庫(kù)文件時(shí)經(jīng)常使用的一個(gè)選項(xiàng)。本文介紹了如何在 CMake 中添加一個(gè)選項(xiàng)來(lái)控制是否將庫(kù)編譯為動(dòng)態(tài)庫(kù),且該選項(xiàng)可以和 CMake 一樣跨平臺(tái)使用。
本文并不從動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)的起源開(kāi)始講起,因此有些預(yù)備知識(shí)需要你提前了解。這些知識(shí)不需要你深刻的了解并實(shí)踐過(guò),只需看過(guò)相關(guān)文章有所了解即可。這些預(yù)備知識(shí)包括:
C++ 基本編程知識(shí)。
什么是動(dòng)態(tài)庫(kù)。
動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)的區(qū)別。
CMake 是干什么的,CMake 的基本知識(shí)。
上面的預(yù)備知識(shí)可以在網(wǎng)上很容易查閱得到,Static vs Dynamic Libraries 這篇英文文章介紹的也比較詳細(xì),可以作為參考。
主要難點(diǎn)
關(guān)于在 CMake 中添加動(dòng)態(tài)庫(kù)的選項(xiàng),主要的難點(diǎn)就在于 windows 平臺(tái)上的改動(dòng)較大。在 linux 平臺(tái)下,動(dòng)態(tài)庫(kù)和靜態(tài)庫(kù)的源代碼是完全一樣的,只需要修改編譯參數(shù)即可。而 windows 平臺(tái)下,如果想把靜態(tài)庫(kù)變成動(dòng)態(tài)庫(kù)的話,所有的源碼都需要改變!因此如果你之前寫(xiě)了大量代碼而沒(méi)有考慮到 windows 平臺(tái)上編譯動(dòng)態(tài)庫(kù)的話,修改起來(lái)可能會(huì)是一個(gè)很大的工程。本文將以一個(gè)簡(jiǎn)單的實(shí)例來(lái)介紹如何編寫(xiě)一個(gè)可跨平臺(tái)的帶動(dòng)靜態(tài)編譯參數(shù)的 CMake 項(xiàng)目。示例代碼可以在 這里 查看。
Windows 平臺(tái)下的動(dòng)態(tài)庫(kù)導(dǎo)入導(dǎo)出
前面我們提到過(guò),在 Windows 平臺(tái)中生成動(dòng)態(tài)庫(kù)其源碼和靜態(tài)庫(kù)是不同的。在 Windows 平臺(tái)中,我們導(dǎo)出動(dòng)態(tài)庫(kù)時(shí),除了會(huì)生成 .dll 動(dòng)態(tài)庫(kù)之外還會(huì)生成一個(gè) .lib 文件。這個(gè) .lib 文件和靜態(tài)庫(kù)的 .lib 文件不同,它里面并不保存代碼生成的二進(jìn)制文件,而是所有需要導(dǎo)出符號(hào)的符號(hào)表。因此這個(gè) .lib 文件和編譯靜態(tài)庫(kù)生成的 .lib 文件相比會(huì)小很多。而這個(gè)導(dǎo)出的符號(hào)表是需要我們?cè)谠创a中進(jìn)行指定的。如果我們希望將將一個(gè)符號(hào)(symbol)導(dǎo)出(這里的符號(hào)可以指類(lèi)、函數(shù)等各種類(lèi)型),需要在其前面加上 __declspec(dllexport) 標(biāo)志。這樣這個(gè)符號(hào)的相關(guān)信息就會(huì)導(dǎo)出的 .lib 中的符號(hào)表中了。如果我們的源碼中沒(méi)有任何 __declspec(dllexport) 的話,我們依然可以成功的編譯出動(dòng)態(tài)庫(kù),但是并不會(huì)生成保存符號(hào)表的.lib 文件。這也是在 Windows 平臺(tái)下編譯動(dòng)態(tài)庫(kù)經(jīng)常出現(xiàn)的問(wèn)題,如果我們的源碼是在 Linux 平臺(tái)下編寫(xiě)的話,更是很容易忘記修改源碼。以下是一個(gè)導(dǎo)出 MyClass 的例子:
class __declspec(dllexport) MyClass {
public:
static void MyPrint();
};
除了導(dǎo)出符號(hào)標(biāo)識(shí)符 __declspec(dllexport) 以外,我們作為用戶(hù)使用動(dòng)態(tài)庫(kù)的時(shí)候,對(duì)應(yīng)頭文件中的符號(hào)還需要有 __declspec(dllimport) 標(biāo)識(shí)符來(lái)表示這個(gè)符號(hào)是從動(dòng)態(tài)庫(kù)導(dǎo)入的。對(duì)應(yīng)上面的 MyClass 這個(gè)例子,我們包含的頭文件應(yīng)該有以下內(nèi)容:
class __declspec(dllimport) MyClass {
public:
static void MyPrint();
};
一般對(duì)于一個(gè)庫(kù)文件我們并不想對(duì)導(dǎo)入和導(dǎo)出分別寫(xiě)兩個(gè)幾乎同樣的頭文件,因此往往使用宏來(lái)替代直接使用 __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)出)這個(gè)庫(kù)的時(shí)候,給編譯器添加 MY_LIB_EXPORTS 宏。而在使用該庫(kù)的時(shí)候什么都不定義即可。
編寫(xiě) export 頭文件
了解了 windows 平臺(tái)下頭文件的導(dǎo)出規(guī)則之后,現(xiàn)在我們來(lái)編寫(xiě)一個(gè)名為 my_lib_export.h 的頭文件來(lái)專(zhuān)門(mén)的控制 MY_LIB_API 宏的定義。注意到上面的討論只是在 Windows 平臺(tái)以及編寫(xiě)動(dòng)態(tài)庫(kù)時(shí)才會(huì)發(fā)生,對(duì)應(yīng) Linux 平臺(tái)以及編寫(xiě)靜態(tài)庫(kù)時(shí),我們按照教科書(shū)上的定義按部就班的寫(xiě)我們的 C++ 代碼即可。為了使 MY_LIB_API 的定義同時(shí)考慮到 Linux 平臺(tái)以及靜態(tài)庫(kù)的的情況。這里豐富 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 宏來(lái)判斷是否為導(dǎo)出動(dòng)態(tài)庫(kù)以外,還使用到了編譯器自帶的 _WIN32 宏來(lái)判斷是否是在 windows 平臺(tái)上以及使用了需要我們自己定義的另外一個(gè)宏 MY_LIB_SHARED_BUILD 來(lái)判斷是否正在編譯動(dòng)態(tài)庫(kù)。除了上一節(jié)討論的情況以外,我們均將 MY_LIB_API 的值設(shè)置為了空,即什么都沒(méi)有定義。此時(shí)和普通的類(lèi)定義完全相同。有了這個(gè)頭文件之后,我們只需要在導(dǎo)出符號(hào)表的頭文件中包含該頭文件,就可以使用 MY_LIB_API 宏了。
關(guān)于 MY_LIB_SHARED_BUILD 和 MY_LIB_EXPORTS 宏的定義,我將在下面 CMakeLists.txt 的編寫(xiě)一節(jié)進(jìn)行介紹。最后在多介紹一點(diǎn),事實(shí)上 my_lib_export.h 這個(gè)頭文件是可以通過(guò) CMake 提供的 GenerateExportHeader 命令自動(dòng)生成的??紤]到自動(dòng)生成的額外配置以及生成后的文件多出的無(wú)關(guān)內(nèi)容不易于理解,這里不對(duì)該命令的使用進(jìn)行介紹了,如果有興趣的話,可以自行了解。以下內(nèi)容摘抄自一個(gè)自動(dòng)生成的 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 */
編寫(xiě) CMakeLists 文件
上面的內(nèi)容介紹了如何修改源碼:
編寫(xiě) my_lib_export.h 頭文件在其中定義
MY_LIB_API宏。在需要導(dǎo)出的符號(hào)前,加上
MY_LIB_API宏。
接下來(lái)將介紹如何編寫(xiě) CMakeLists.txt 文件,其實(shí)只需要在 CMakeLists.txt 文件中做兩件事:
添加是否編譯動(dòng)態(tài)庫(kù)選項(xiàng)。
定義相關(guān)的宏幫助源碼“理解”應(yīng)該如何定義
MY_LIB_API宏。
首先頂層 CMakeLists.txt 文件中需要添加如下語(yǔ)句來(lái)添加編譯選項(xiàng):
option(BUILD_SHARED_LIBS "Specifies the type of libraries (SHARED or STATIC) to build" OFF)
其次在編譯庫(kù)的 CMakeLists.txt 文件中需要根據(jù)指定的編譯選項(xiàng),來(lái)定義不同的編譯形式以及宏定義:
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()
這里在編譯動(dòng)態(tài)庫(kù)的時(shí)候會(huì)添加兩個(gè)宏定義 MY_LIB_SHARED_BUILD 以及 MY_LIB_EXPORTS。注意到這里 MY_LIB_EXPORTS 宏定義的訪問(wèn)符為 PRIVATE 即這個(gè)宏定義只是在編譯時(shí)有效。而 MY_LIB_SHARED_BUILD 宏定義的訪問(wèn)符為 PUBLIC ,即無(wú)論是編譯還是安裝后作為庫(kù)文件引用時(shí)均有效。這樣我們就可以保證源碼中 MY_LIB_API 宏被正確定義了。
完整的 CMakeLists.txt 文件和源碼可以在 github 倉(cāng)庫(kù) 中查看。
參考資料: