:靜態(tài)庫(kù)動(dòng)態(tài)庫(kù)實(shí)戰(zhàn)指南)
1. 從一個(gè)真實(shí)場(chǎng)景說(shuō)起為什么你需要搞懂CMake的庫(kù)生成與鏈接如果你寫(xiě)過(guò)C或者C大概率經(jīng)歷過(guò)這樣的階段一開(kāi)始所有代碼都塞進(jìn)一個(gè)main.c或者main.cpp里編譯命令就是一行g(shù)cc main.c -o app簡(jiǎn)單粗暴能跑就行。但項(xiàng)目稍微大一點(diǎn)代碼量上來(lái)了你就會(huì)開(kāi)始琢磨是不是該把一些通用的功能抽出來(lái)單獨(dú)編譯成一個(gè)庫(kù)這樣既能復(fù)用又能讓主程序的編譯速度變快還能把接口和實(shí)現(xiàn)分離別人用你的庫(kù)時(shí)只需要頭文件和二進(jìn)制文件不用關(guān)心內(nèi)部實(shí)現(xiàn)細(xì)節(jié)。這個(gè)思路是對(duì)的但問(wèn)題也隨之而來(lái)庫(kù)怎么生成生成之后怎么讓主程序找到它、鏈接它在Linux環(huán)境下CMake是目前最主流的構(gòu)建工具之一它跨平臺(tái)、語(yǔ)法相對(duì)清晰、社區(qū)生態(tài)好。但很多人在第一次用CMake生成庫(kù)并鏈接庫(kù)的時(shí)候都會(huì)踩一堆坑undefined reference、cannot find -lxxx、運(yùn)行時(shí)error while loading shared libraries……這些報(bào)錯(cuò)信息看著就頭疼。我自己在帶新人的時(shí)候發(fā)現(xiàn)大家最容易卡住的地方不是寫(xiě)代碼而是構(gòu)建系統(tǒng)。代碼邏輯明明沒(méi)問(wèn)題但就是編譯不過(guò)、鏈接不上。所以這篇文章我想把Linux下用CMake生成庫(kù)、鏈接庫(kù)這件事從頭到尾講清楚。不管你是剛接觸CMake的新手還是已經(jīng)用過(guò)但總是靠復(fù)制粘貼配置的老手我都盡量把每個(gè)參數(shù)、每個(gè)步驟背后的“為什么”說(shuō)明白。文章會(huì)覆蓋靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)的生成方式、鏈接時(shí)的路徑處理、運(yùn)行時(shí)動(dòng)態(tài)庫(kù)查找、常見(jiàn)報(bào)錯(cuò)排查以及一些我在實(shí)際項(xiàng)目中總結(jié)出來(lái)的經(jīng)驗(yàn)技巧。你可以把它當(dāng)成一份可以隨時(shí)翻出來(lái)對(duì)照的操作手冊(cè)。2. 先搞清楚基礎(chǔ)概念靜態(tài)庫(kù)與動(dòng)態(tài)庫(kù)到底有什么區(qū)別在動(dòng)手寫(xiě)CMake配置之前有必要先把靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)這兩個(gè)概念理清楚。很多人配置寫(xiě)不對(duì)根本原因是對(duì)這兩種庫(kù)的鏈接機(jī)制理解模糊。2.1 靜態(tài)庫(kù)的工作方式與適用場(chǎng)景靜態(tài)庫(kù)在Linux下通常以.a結(jié)尾全稱(chēng)是archive本質(zhì)上就是一堆.o目標(biāo)文件的打包集合。當(dāng)你把主程序和靜態(tài)庫(kù)鏈接時(shí)鏈接器會(huì)把靜態(tài)庫(kù)中用到的目標(biāo)文件直接復(fù)制到最終的可執(zhí)行文件里。這意味著最終的可執(zhí)行文件體積會(huì)變大但好處是它不依賴(lài)外部庫(kù)文件拿到哪里都能跑。靜態(tài)庫(kù)的生成過(guò)程分兩步先把源文件編譯成目標(biāo)文件.o然后用ar命令打包。在CMake里你只需要用add_library命令并指定STATIC類(lèi)型CMake會(huì)自動(dòng)幫你完成這兩步。靜態(tài)庫(kù)適合什么場(chǎng)景我個(gè)人的經(jīng)驗(yàn)是小工具、嵌入式環(huán)境、需要單文件分發(fā)的場(chǎng)景以及你不想讓用戶(hù)操心動(dòng)態(tài)庫(kù)路徑的情況。缺點(diǎn)是如果多個(gè)程序都鏈接了同一個(gè)靜態(tài)庫(kù)每個(gè)程序都會(huì)有一份庫(kù)代碼的副本內(nèi)存和磁盤(pán)占用會(huì)重復(fù)。2.2 動(dòng)態(tài)庫(kù)的工作方式與適用場(chǎng)景動(dòng)態(tài)庫(kù)在Linux下以.so結(jié)尾全稱(chēng)是shared object。它和靜態(tài)庫(kù)最大的區(qū)別在于鏈接階段只是記錄了對(duì)動(dòng)態(tài)庫(kù)的引用真正的代碼加載發(fā)生在程序啟動(dòng)時(shí)或者運(yùn)行時(shí)通過(guò)dlopen手動(dòng)加載。多個(gè)程序可以共享同一個(gè)動(dòng)態(tài)庫(kù)的內(nèi)存映射節(jié)省系統(tǒng)資源。動(dòng)態(tài)庫(kù)的生成需要編譯時(shí)加上-fPIC選項(xiàng)也就是位置無(wú)關(guān)代碼。這是因?yàn)閯?dòng)態(tài)庫(kù)被加載到內(nèi)存的地址是不確定的代碼必須能在任意地址執(zhí)行。CMake里用add_library指定SHARED類(lèi)型時(shí)會(huì)自動(dòng)幫你加上-fPIC。動(dòng)態(tài)庫(kù)的優(yōu)點(diǎn)是節(jié)省資源、方便更新替換.so文件即可不用重新編譯主程序缺點(diǎn)是部署時(shí)需要考慮庫(kù)的查找路徑問(wèn)題否則程序啟動(dòng)時(shí)會(huì)報(bào)找不到庫(kù)。2.3 兩種庫(kù)的對(duì)比與選型建議對(duì)比維度靜態(tài)庫(kù).a動(dòng)態(tài)庫(kù).so鏈接時(shí)機(jī)編譯鏈接時(shí)復(fù)制代碼運(yùn)行時(shí)加載可執(zhí)行文件體積較大較小部署依賴(lài)無(wú)外部依賴(lài)需要庫(kù)文件存在更新方式需重新編譯主程序替換庫(kù)文件即可內(nèi)存占用每個(gè)程序獨(dú)立副本多程序共享編譯選項(xiàng)無(wú)特殊要求需要-fPIC適用場(chǎng)景小工具、嵌入式、單文件分發(fā)大型項(xiàng)目、多程序共享、插件架構(gòu)選型的時(shí)候我一般遵循這樣的原則如果是公司內(nèi)部的基礎(chǔ)組件多個(gè)項(xiàng)目都會(huì)用那就做成動(dòng)態(tài)庫(kù)方便統(tǒng)一升級(jí)如果是給外部交付的單文件工具那就靜態(tài)鏈接省得用戶(hù)配環(huán)境如果項(xiàng)目對(duì)啟動(dòng)速度有極致要求靜態(tài)庫(kù)會(huì)略快一點(diǎn)點(diǎn)因?yàn)槭∪チ藙?dòng)態(tài)鏈接的過(guò)程。3. CMake生成庫(kù)的核心命令與參數(shù)詳解理解了靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)的區(qū)別之后我們來(lái)看CMake里具體怎么操作。這一部分我會(huì)把每個(gè)關(guān)鍵命令拆開(kāi)講包括參數(shù)的含義和我自己在使用中總結(jié)的注意事項(xiàng)。3.1 add_library命令的完整用法add_library是CMake生成庫(kù)的核心命令基本語(yǔ)法是add_library(庫(kù)名稱(chēng) [STATIC | SHARED | MODULE] [源文件...])庫(kù)名稱(chēng)不需要加lib前綴和.a/.so后綴CMake會(huì)自動(dòng)處理。比如你寫(xiě)add_library(mymath STATIC math.cpp)最終生成的文件是libmymath.a。STATIC和SHARED分別對(duì)應(yīng)靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)。如果不指定類(lèi)型CMake會(huì)根據(jù)BUILD_SHARED_LIBS變量的值來(lái)決定默認(rèn)是STATIC。我建議在項(xiàng)目里顯式指定類(lèi)型避免因?yàn)榄h(huán)境變量不同導(dǎo)致構(gòu)建結(jié)果不一致。MODULE類(lèi)型比較特殊它生成的是不會(huì)被自動(dòng)鏈接的動(dòng)態(tài)庫(kù)通常用于插件場(chǎng)景需要配合dlopen手動(dòng)加載。一般項(xiàng)目里用得少但如果你要做插件架構(gòu)就會(huì)用到它。還有一個(gè)細(xì)節(jié)源文件列表可以寫(xiě)多個(gè)文件也可以用變量代替。比如set(MYMATH_SOURCES math.cpp advanced_math.cpp) add_library(mymath STATIC ${MYMATH_SOURCES})這樣寫(xiě)的好處是源文件列表集中管理后續(xù)增刪文件不容易漏改。3.2 頭文件路徑與安裝規(guī)則的配置生成庫(kù)的時(shí)候通常需要指定頭文件路徑讓編譯器能找到你的.h文件。用target_include_directories命令target_include_directories(mymath PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include $INSTALL_INTERFACE:include )這里PUBLIC的意思是這個(gè)包含路徑既用于編譯庫(kù)本身也會(huì)傳遞給鏈接這個(gè)庫(kù)的目標(biāo)。BUILD_INTERFACE和INSTALL_INTERFACE是生成器表達(dá)式分別指定構(gòu)建時(shí)和安裝后的路徑。這樣寫(xiě)的好處是當(dāng)你的庫(kù)被安裝到系統(tǒng)目錄后其他項(xiàng)目通過(guò)find_package找到它時(shí)能自動(dòng)獲得正確的頭文件路徑。安裝規(guī)則用install命令install(TARGETS mymath ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin ) install(DIRECTORY include/ DESTINATION include)ARCHIVE對(duì)應(yīng)靜態(tài)庫(kù)LIBRARY對(duì)應(yīng)動(dòng)態(tài)庫(kù)RUNTIME對(duì)應(yīng)可執(zhí)行文件Windows下的DLL也會(huì)放到RUNTIME。這樣配置之后執(zhí)行make install就會(huì)把庫(kù)文件和頭文件分別放到lib和include目錄下。3.3 版本號(hào)與SONAME的配置要點(diǎn)動(dòng)態(tài)庫(kù)有一個(gè)很重要的概念叫SONAMEShared Object Name它決定了程序運(yùn)行時(shí)查找的是哪個(gè)版本的庫(kù)。在CMake里通過(guò)set_target_properties配置set_target_properties(mymath PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} )假設(shè)版本號(hào)是1.2.3SOVERSION是1那么生成的文件會(huì)有三個(gè)libmymath.so.1.2.3實(shí)際文件、libmymath.so.1SONAME軟鏈接、libmymath.so開(kāi)發(fā)用軟鏈接。程序鏈接時(shí)記錄的是SONAME也就是libmymath.so.1運(yùn)行時(shí)加載器會(huì)去找這個(gè)文件。這樣做的好處是當(dāng)你升級(jí)到1.3.0時(shí)SONAME還是libmymath.so.1舊程序不用重新編譯就能用新庫(kù)如果升級(jí)到2.0.0SOVERSION變成2舊程序仍然找libmymath.so.1不會(huì)因?yàn)榻涌诓患嫒荻罎ⅰW⒁馊绻悴辉O(shè)置SOVERSIONCMake默認(rèn)不會(huì)生成SONAME軟鏈接程序鏈接時(shí)記錄的就是完整路徑或者libmymath.so這會(huì)給后續(xù)升級(jí)帶來(lái)麻煩。所以只要做動(dòng)態(tài)庫(kù)就一定要配VERSION和SOVERSION。4. 鏈接庫(kù)的完整實(shí)操流程與參數(shù)計(jì)算庫(kù)生成好了接下來(lái)就是怎么讓主程序鏈接它。這一部分我按照從簡(jiǎn)單到復(fù)雜的順序把鏈接方式分成幾種情況來(lái)講每種情況都給出完整的CMake配置和操作步驟。4.1 同一項(xiàng)目?jī)?nèi)鏈接target_link_libraries的基本用法如果庫(kù)和主程序在同一個(gè)CMake項(xiàng)目里鏈接非常簡(jiǎn)單add_executable(myapp main.cpp) target_link_libraries(myapp PRIVATE mymath)PRIVATE表示這個(gè)鏈接關(guān)系只用于編譯myapp本身不會(huì)傳遞給其他依賴(lài)myapp的目標(biāo)。如果是庫(kù)依賴(lài)庫(kù)的情況比如mymath依賴(lài)mylog那mymath鏈接mylog時(shí)應(yīng)該用PRIVATE還是PUBLIC這取決于mymath的頭文件里是否暴露了mylog的類(lèi)型。如果mymath.h里包含了mylog.h那就必須用PUBLIC否則鏈接mymath的程序會(huì)因?yàn)檎也坏絤ylog的符號(hào)而報(bào)錯(cuò)。如果mylog只在mymath.cpp里用那就用PRIVATE避免不必要的依賴(lài)傳遞。這個(gè)PRIVATE/PUBLIC/INTERFACE的區(qū)分是CMake現(xiàn)代用法里很重要的部分用好了能讓依賴(lài)關(guān)系清晰編譯速度也會(huì)更快。4.2 鏈接外部庫(kù)find_library與link_directories的取舍當(dāng)庫(kù)不在同一個(gè)項(xiàng)目里而是已經(jīng)安裝在系統(tǒng)的某個(gè)位置就需要告訴CMake去哪里找。有兩種方式第一種是用find_libraryfind_library(MYMATH_LIB NAMES mymath PATHS /opt/mymath/lib) if(NOT MYMATH_LIB) message(FATAL_ERROR mymath library not found) endif() target_link_libraries(myapp PRIVATE ${MYMATH_LIB})第二種是用link_directories加target_link_librarieslink_directories(/opt/mymath/lib) target_link_libraries(myapp PRIVATE mymath)我強(qiáng)烈建議用第一種方式。link_directories是全局生效的會(huì)影響之后所有目標(biāo)的鏈接路徑容易造成污染和意外鏈接到錯(cuò)誤版本的庫(kù)。find_library是精確查找找到之后把完整路徑傳給target_link_libraries依賴(lài)關(guān)系明確排查問(wèn)題也方便。如果庫(kù)有對(duì)應(yīng)的CMake配置文件比如mymathConfig.cmake那更好的方式是用find_packagefind_package(mymath REQUIRED) target_link_libraries(myapp PRIVATE mymath::mymath)這種方式能自動(dòng)處理頭文件路徑、庫(kù)路徑、依賴(lài)關(guān)系是最推薦的做法。但前提是庫(kù)的開(kāi)發(fā)者提供了配置文件這個(gè)我們后面會(huì)講怎么生成。4.3 運(yùn)行時(shí)動(dòng)態(tài)庫(kù)查找RPATH與LD_LIBRARY_PATH的處理動(dòng)態(tài)庫(kù)鏈接成功之后編譯不會(huì)報(bào)錯(cuò)但運(yùn)行的時(shí)候可能會(huì)遇到error while loading shared libraries: libmymath.so.1: cannot open shared object file這是因?yàn)閯?dòng)態(tài)鏈接器在運(yùn)行時(shí)找不到庫(kù)文件。默認(rèn)情況下動(dòng)態(tài)鏈接器會(huì)搜索/lib、/usr/lib這些系統(tǒng)目錄以及LD_LIBRARY_PATH環(huán)境變量指定的目錄。如果你的庫(kù)安裝在/opt/mymath/lib那就不在默認(rèn)搜索路徑里。解決辦法有幾種第一種是設(shè)置LD_LIBRARY_PATHexport LD_LIBRARY_PATH/opt/mymath/lib:$LD_LIBRARY_PATH ./myapp這種方式簡(jiǎn)單但每次運(yùn)行都要設(shè)置而且如果多個(gè)項(xiàng)目依賴(lài)不同版本的庫(kù)容易沖突。第二種是在CMake里設(shè)置RPATHset_target_properties(myapp PROPERTIES BUILD_RPATH /opt/mymath/lib INSTALL_RPATH /opt/mymath/lib )RPATH會(huì)被寫(xiě)入可執(zhí)行文件運(yùn)行時(shí)動(dòng)態(tài)鏈接器會(huì)優(yōu)先從RPATH指定的目錄查找。BUILD_RPATH用于構(gòu)建時(shí)INSTALL_RPATH用于安裝后。這種方式比LD_LIBRARY_PATH更可靠因?yàn)樗菍?xiě)死在二進(jìn)制文件里的不受環(huán)境變量影響。第三種是把庫(kù)安裝到系統(tǒng)默認(rèn)搜索路徑比如/usr/local/lib然后運(yùn)行l(wèi)dconfig更新緩存。這種方式適合系統(tǒng)級(jí)安裝但需要root權(quán)限而且可能影響其他程序。我一般推薦用RPATH的方式特別是項(xiàng)目部署到多臺(tái)機(jī)器時(shí)RPATH能保證程序在任何環(huán)境下都能找到正確的庫(kù)。4.4 參數(shù)計(jì)算實(shí)例從編譯命令反推CMake配置有時(shí)候我們需要從手寫(xiě)的編譯命令反推CMake配置或者反過(guò)來(lái)驗(yàn)證CMake生成的命令是否正確。舉個(gè)例子假設(shè)手寫(xiě)編譯命令是這樣的gcc main.c -o app -I/opt/mymath/include -L/opt/mymath/lib -lmymath -Wl,-rpath,/opt/mymath/lib對(duì)應(yīng)的CMake配置就是find_library(MYMATH_LIB NAMES mymath PATHS /opt/mymath/lib) target_include_directories(myapp PRIVATE /opt/mymath/include) target_link_libraries(myapp PRIVATE ${MYMATH_LIB}) set_target_properties(myapp PROPERTIES INSTALL_RPATH /opt/mymath/lib)你可以用make VERBOSE1來(lái)查看CMake實(shí)際執(zhí)行的編譯和鏈接命令對(duì)比一下就能確認(rèn)配置是否正確。這個(gè)技巧在排查鏈接問(wèn)題時(shí)非常有用。5. 常見(jiàn)報(bào)錯(cuò)與排查技巧實(shí)錄這一部分是我在實(shí)際項(xiàng)目里踩過(guò)的坑和總結(jié)出來(lái)的排查方法。鏈接問(wèn)題往往報(bào)錯(cuò)信息很模糊但只要掌握套路定位起來(lái)并不難。5.1 undefined reference符號(hào)找不到的幾種原因undefined reference to xxx是最常見(jiàn)的鏈接錯(cuò)誤。原因通常有這幾種第一種是庫(kù)沒(méi)有鏈接上。檢查target_link_libraries里是否包含了對(duì)應(yīng)的庫(kù)庫(kù)名拼寫(xiě)是否正確。注意CMake里寫(xiě)的是mymath不是libmymath.a。第二種是鏈接順序問(wèn)題。在Linux下鏈接器從左到右處理庫(kù)如果庫(kù)A依賴(lài)庫(kù)B那A必須寫(xiě)在B前面。CMake的target_link_libraries會(huì)自動(dòng)處理依賴(lài)順序但如果你手動(dòng)用-l參數(shù)就要注意順序。第三種是C和C混合編譯時(shí)的extern C問(wèn)題。C編譯器會(huì)對(duì)函數(shù)名進(jìn)行name mangling如果C代碼調(diào)用C庫(kù)的函數(shù)或者反過(guò)來(lái)就需要用extern C包裹聲明。這個(gè)坑在混合編程時(shí)非常常見(jiàn)。第四種是庫(kù)的ABI不兼容。比如用不同版本的編譯器編譯的庫(kù)或者C標(biāo)準(zhǔn)庫(kù)版本不一致都可能導(dǎo)致符號(hào)找不到。這種情況比較隱蔽需要檢查編譯器和標(biāo)準(zhǔn)庫(kù)版本。5.2 cannot find -lxxx庫(kù)路徑配置錯(cuò)誤的排查cannot find -lmymath表示鏈接器在搜索路徑里找不到libmymath.so或libmymath.a。排查步驟先確認(rèn)庫(kù)文件確實(shí)存在用ls看一下路徑對(duì)不對(duì)。然后確認(rèn)target_link_libraries里寫(xiě)的庫(kù)名和實(shí)際文件名是否匹配。如果庫(kù)文件是libmymath.a那CMake里寫(xiě)mymath就對(duì)了。如果庫(kù)文件是libmymath_core.a那就要寫(xiě)mymath_core。再確認(rèn)搜索路徑是否包含庫(kù)所在目錄。用find_library的話(huà)檢查PATHS參數(shù)是否正確用link_directories的話(huà)檢查路徑是否拼寫(xiě)錯(cuò)誤??梢栽贑Make里加message(STATUS MYMATH_LIB${MYMATH_LIB})來(lái)打印查找結(jié)果。還有一個(gè)容易忽略的點(diǎn)32位和64位庫(kù)混用。如果你的系統(tǒng)是64位但庫(kù)是32位編譯的鏈接時(shí)會(huì)報(bào)找不到。用file libmymath.a可以查看庫(kù)的架構(gòu)信息。5.3 運(yùn)行時(shí)找不到動(dòng)態(tài)庫(kù)三種解決方案對(duì)比前面提到了error while loading shared libraries的問(wèn)題這里把三種解決方案做個(gè)對(duì)比方案優(yōu)點(diǎn)缺點(diǎn)適用場(chǎng)景LD_LIBRARY_PATH配置簡(jiǎn)單立即生效環(huán)境變量污染多版本沖突臨時(shí)調(diào)試RPATH寫(xiě)入二進(jìn)制不受環(huán)境影響路徑寫(xiě)死遷移需重新編譯固定部署路徑ldconfig系統(tǒng)級(jí)生效所有程序可用需要root權(quán)限影響全局系統(tǒng)級(jí)安裝我個(gè)人的習(xí)慣是開(kāi)發(fā)階段用LD_LIBRARY_PATH快速驗(yàn)證部署階段用RPATH保證可靠性如果是給系統(tǒng)安裝庫(kù)那就用ldconfig。5.4 常見(jiàn)問(wèn)題速查表報(bào)錯(cuò)信息可能原因排查方法解決方案undefined reference庫(kù)未鏈接/順序錯(cuò)誤/extern C缺失檢查target_link_libraries和頭文件聲明補(bǔ)鏈接、調(diào)整順序、加extern Ccannot find -lxxx庫(kù)路徑不對(duì)/庫(kù)名拼寫(xiě)錯(cuò)誤用find_library打印結(jié)果修正路徑或庫(kù)名error while loading shared libraries運(yùn)行時(shí)找不到.so用ldd查看依賴(lài)設(shè)置RPATH或LD_LIBRARY_PATHversion GLIBCXX_xxx not foundC標(biāo)準(zhǔn)庫(kù)版本不匹配檢查編譯器和標(biāo)準(zhǔn)庫(kù)版本統(tǒng)一編譯環(huán)境或靜態(tài)鏈接標(biāo)準(zhǔn)庫(kù)cannot open shared object file庫(kù)文件不存在或權(quán)限不足ls檢查文件檢查權(quán)限安裝庫(kù)文件或修改權(quán)限提示遇到鏈接問(wèn)題時(shí)先用ldd查看可執(zhí)行文件的依賴(lài)關(guān)系再用nm查看庫(kù)里的符號(hào)最后用make VERBOSE1查看實(shí)際編譯命令。這三板斧能解決大部分問(wèn)題。6. 進(jìn)階技巧讓庫(kù)更專(zhuān)業(yè)、更好用基礎(chǔ)操作掌握之后這一部分分享一些進(jìn)階技巧讓你的庫(kù)更容易被其他項(xiàng)目集成也更符合工程規(guī)范。6.1 生成CMake配置文件讓find_package開(kāi)箱即用如果你希望別人用find_package(mymath)就能找到你的庫(kù)需要生成兩個(gè)文件mymathConfig.cmake和mymathConfigVersion.cmake。CMake提供了configure_package_config_file和write_basic_package_version_file兩個(gè)宏來(lái)簡(jiǎn)化這個(gè)過(guò)程。include(CMakePackageConfigHelpers) configure_package_config_file( ${CMAKE_CURRENT_SOURCE_DIR}/cmake/mymathConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/mymathConfig.cmake INSTALL_DESTINATION lib/cmake/mymath ) write_basic_package_version_file( ${CMAKE_CURRENT_BINARY_DIR}/mymathConfigVersion.cmake VERSION ${PROJECT_VERSION} COMPATIBILITY SameMajorVersion ) install(FILES ${CMAKE_CURRENT_BINARY_DIR}/mymathConfig.cmake ${CMAKE_CURRENT_BINARY_DIR}/mymathConfigVersion.cmake DESTINATION lib/cmake/mymath )mymathConfig.cmake.in的內(nèi)容大概是PACKAGE_INIT include(${CMAKE_CURRENT_LIST_DIR}/mymathTargets.cmake)然后導(dǎo)出目標(biāo)install(TARGETS mymath EXPORT mymathTargets ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION bin ) install(EXPORT mymathTargets FILE mymathTargets.cmake NAMESPACE mymath:: DESTINATION lib/cmake/mymath )這樣配置之后其他項(xiàng)目用find_package(mymath REQUIRED)就能找到你的庫(kù)然后用target_link_libraries(app PRIVATE mymath::mymath)鏈接頭文件路徑、庫(kù)路徑、依賴(lài)關(guān)系全部自動(dòng)處理。這是最專(zhuān)業(yè)的做法也是開(kāi)源庫(kù)的標(biāo)配。6.2 用Object Library減少重復(fù)編譯如果你的庫(kù)有多個(gè)目標(biāo)共享同一批源文件可以用Object Library來(lái)避免重復(fù)編譯add_library(mymath_obj OBJECT math.cpp advanced_math.cpp) target_include_directories(mymath_obj PUBLIC include) add_library(mymath_static STATIC $TARGET_OBJECTS:mymath_obj) add_library(mymath_shared SHARED $TARGET_OBJECTS:mymath_obj)這樣math.cpp和advanced_math.cpp只編譯一次生成的目標(biāo)文件同時(shí)用于靜態(tài)庫(kù)和動(dòng)態(tài)庫(kù)編譯時(shí)間能省不少。這個(gè)技巧在大型項(xiàng)目里特別有用。6.3 編譯選項(xiàng)的精細(xì)控制與調(diào)試符號(hào)生成庫(kù)的時(shí)候編譯選項(xiàng)的控制也很重要。比如調(diào)試版本需要加-g發(fā)布版本需要加-O2這些可以通過(guò)target_compile_options來(lái)配置target_compile_options(mymath PRIVATE $$CONFIG:Debug:-g -O0 $$CONFIG:Release:-O2 -DNDEBUG )$CONFIG:Debug是生成器表達(dá)式根據(jù)構(gòu)建類(lèi)型選擇不同的選項(xiàng)。這樣一套配置就能同時(shí)支持Debug和Release構(gòu)建不用手動(dòng)改CMakeLists。另外動(dòng)態(tài)庫(kù)默認(rèn)會(huì)導(dǎo)出所有符號(hào)如果你只想導(dǎo)出指定的接口可以用-fvisibilityhidden配合__attribute__((visibility(default)))來(lái)控制。這樣能減小庫(kù)的體積也能避免符號(hào)沖突。6.4 跨平臺(tái)構(gòu)建的注意事項(xiàng)雖然這篇文章主要講Linux但CMake的優(yōu)勢(shì)就是跨平臺(tái)。如果你希望庫(kù)能在Windows和macOS上也能構(gòu)建需要注意幾點(diǎn)Windows下動(dòng)態(tài)庫(kù)需要顯式導(dǎo)出符號(hào)通常用__declspec(dllexport)和__declspec(dllimport)可以定義一個(gè)宏來(lái)統(tǒng)一處理if(WIN32) target_compile_definitions(mymath PRIVATE MYMATH_EXPORTS) endif()然后在頭文件里#ifdef _WIN32 #ifdef MYMATH_EXPORTS #define MYMATH_API __declspec(dllexport) #else #define MYMATH_API __declspec(dllimport) #endif #else #define MYMATH_API __attribute__((visibility(default))) #endifmacOS下動(dòng)態(tài)庫(kù)是.dylib安裝名稱(chēng)install_name需要用rpath來(lái)配置CMake的INSTALL_RPATH在macOS下也適用但語(yǔ)法略有不同。這些細(xì)節(jié)在跨平臺(tái)項(xiàng)目里都需要注意。7. 一個(gè)完整可復(fù)現(xiàn)的項(xiàng)目示例說(shuō)了這么多理論最后給一個(gè)完整的項(xiàng)目示例你可以直接復(fù)制到本地運(yùn)行驗(yàn)證整個(gè)流程。7.1 項(xiàng)目目錄結(jié)構(gòu)mymath_project/ ├── CMakeLists.txt ├── include/ │ └── mymath.h ├── src/ │ ├── math.cpp │ └── advanced_math.cpp └── app/ └── main.cpp7.2 庫(kù)的CMakeLists.txt配置cmake_minimum_required(VERSION 3.16) project(mymath VERSION 1.0.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(mymath SHARED src/math.cpp src/advanced_math.cpp ) target_include_directories(mymath PUBLIC $BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include $INSTALL_INTERFACE:include ) set_target_properties(mymath PROPERTIES VERSION ${PROJECT_VERSION} SOVERSION ${PROJECT_VERSION_MAJOR} ) install(TARGETS mymath LIBRARY DESTINATION lib ARCHIVE DESTINATION lib RUNTIME DESTINATION bin ) install(DIRECTORY include/ DESTINATION include)7.3 主程序的鏈接配置add_executable(myapp app/main.cpp) target_link_libraries(myapp PRIVATE mymath)7.4 編譯、安裝與運(yùn)行驗(yàn)證mkdir build cd build cmake .. -DCMAKE_INSTALL_PREFIX/opt/mymath make make install # 運(yùn)行程序 /opt/mymath/bin/myapp如果運(yùn)行時(shí)提示找不到動(dòng)態(tài)庫(kù)檢查/opt/mymath/lib是否在RPATH里或者臨時(shí)設(shè)置LD_LIBRARY_PATH驗(yàn)證。7.5 用ldd和nm驗(yàn)證鏈接結(jié)果# 查看可執(zhí)行文件依賴(lài)的動(dòng)態(tài)庫(kù) ldd /opt/mymath/bin/myapp # 查看庫(kù)中導(dǎo)出的符號(hào) nm -D /opt/mymath/lib/libmymath.so # 查看庫(kù)的SONAME objdump -p /opt/mymath/lib/libmymath.so | grep SONAME這幾個(gè)命令能幫你確認(rèn)庫(kù)是否正確生成、程序是否正確鏈接、運(yùn)行時(shí)是否能找到庫(kù)。8. 個(gè)人經(jīng)驗(yàn)總結(jié)與后續(xù)擴(kuò)展方向我在實(shí)際項(xiàng)目里用CMake管理庫(kù)的生成和鏈接最大的體會(huì)是配置要顯式依賴(lài)要清晰路徑要可控。顯式指定庫(kù)類(lèi)型、顯式聲明依賴(lài)關(guān)系、顯式設(shè)置RPATH能避免很多“換個(gè)環(huán)境就編譯不過(guò)”的問(wèn)題。另外盡早引入find_package和配置文件導(dǎo)出機(jī)制雖然前期麻煩一點(diǎn)但項(xiàng)目變大之后會(huì)省很多事。后續(xù)如果你想繼續(xù)深入可以研究這幾個(gè)方向用CPack打包庫(kù)的安裝包用CTest做單元測(cè)試集成用Conan或vcpkg管理第三方依賴(lài)以及用CMake的FetchContent模塊自動(dòng)下載和構(gòu)建依賴(lài)庫(kù)。這些都是在實(shí)際工程中很實(shí)用的技能等基礎(chǔ)打牢之后可以逐步嘗試。