C++ 自分用新手筆記3 : CMake基礎 II — 靜態庫、動態庫編譯

#引言

上一篇文章寫了關於無庫的CMake編譯方式,而次要寫的是補完CMake編譯出函式庫的方法以及函式庫使用方式。

#函式庫的目的

    前情提要一下,在上一篇文章中我寫了關於如何使用CMake直接編譯C++源代碼,簡明扼要的說就是要把一組一組的.cpp與.hpp都組合編譯成.o檔,再把所有的.o檔連結在一起,便完成最終可執檔的編譯。
    這個方法固然方便且簡單,然而這個方法也存在不少的問題,例如如果我要把部分我寫的
函式公開給其他人使用,但又不想透露我的源代碼給別人知道的話,那麼把該些函式的.cpp與.hpp等檔案給別人顯然並非一個有效的做法。
    比較簡單的做法會是把一系列的.cpp與.hpp都組合成.o,再把.o與.hpp給別人的話就可以在隱藏源代碼的前提下把函式給別人使用。
    而為了把上述的方法簡化,並且減少使用者在連結大量.o檔案的困擾(要知道在C++剛開發出來時編譯可沒現在那麼方便),大量的.o檔案被封裝在一個單一檔案中,使用者在編譯時也只需要連結一個檔案即可,那便是函式庫的概念。就算是一些開源的函式庫也會把源代碼編導成.lib或.a發佈,為的也是可以方便使用者的使用。

        **新手HINT : 對於不存在外來函式庫的小型專案,無論是用.o直接連結至main.o上,還是
            要把部分代碼組合成.lib或.a後再連結至main.o上,本質和成品上都是相同的,只是步驟不一樣罷了。

#函式庫的種類

首先,函式庫主要分為兩大類型,一是靜態庫,二是動態庫。兩者的差異主主要在於程式編譯後會被封裝於程式內還是程式外。

    靜態庫最大的優點是與動態庫相比在使用上的便利,靜態庫基本上就是一個被封裝起來的代碼Bundle,使用上和直接連結.o檔案雖不儘相同,但也算是一脈相承了,隨後談到使用方法時會也一並提到,這裡便不作過多闡述。但缺點也是一目瞭然,因為靜態庫的本質就是一堆的.o檔案的組合包,因此編譯完成後所有靜態庫內的程式碼也會被一併封裝至可執行檔裡,使得可執行檔的容量大小變大不小,對現在這個隨便一個硬碟都幾TB的時代來說沒什麼,但對以前那個惜容量如金的時代影響就會大上不少,也因此能夠一庫多用的動態庫被創造了出來。

        **新手HINT :靜態庫其檔案使用的副檔名為Windows上的.lib或Linux/MaxOs上的.a。

    動態庫與靜態庫的最大的差異在於靜態庫在編譯的過程中會一併被封裝至可執行檔中,而動態庫則不會,一般來說動態庫會獨立存在於可執行檔之外。在編譯可執行檔的過程中基本上也不會觸碰到動態庫的檔案,反而是會在編寫C++程式碼中調度,而不會在編譯的過程中調度,也因此,動態庫的使用方式遠比靜態庫的使用更加不直覺。

        **新手HINT :動態庫其檔案使用的副檔名為Windows上的.dll或Linux/MaxOs上的.so。

#靜態庫編譯

範例資料夾結構 : 

    以上為範例用的專案,內容十分簡單,一個裝源代碼的src資料夾,一個裝CMake檔案的build資料夾。src資料夾有各兩個.cpp和.hpp,在API.cpp及.hpp定義了add_two_numbers的函式,以及在API2.cpp及.hpp則定義了add_three_numbers的函式。CMakeList.txt中只需要一句簡單的add_library(Lib_Name STATIC path/to/cpp path2/to/cpp)便足夠了。







以上為CMake的指令,一樣先移動至build資料夾,再用cmake .. --fresh讀取CMakeLists.txt,最後用cmake --build . 來完成編譯。

要編譯出一個靜態庫就是這麼簡單。add_library => cmake --build . 搞定了。
如果編譯完成後,在路徑 build/Debug 中有出現 Add2Lib.lib 以及 Add2Lib.pdb就代表編譯成功。

#靜態庫使用

完成靜態庫編譯後,是時候使用它了。
接下來有兩種可能性,一是該函式庫被交給其他開發者手上使用,二是該函式庫只是編譯整個程式的步驟之一,並沒有把其交到其他的開發者手中的打算。
由於我相信大多數使用到函式庫的情形都是可能性一,因此這裡就主要示範可能性一,但可能性二的情形也只是把接下來會提到的CMake語句合併到編譯函式庫時用到的CMakeLists.txt便可,並不會太複雜,融會貫通一下便可。

以上是使用該函式庫的示範專案結構,與先前使用過的資料結構差不多,但多新增了lib以及include資料夾,在上一部分編譯出來的.lib檔需要拷貝至lib資料夾,而先前使用過的.hpp頭文件則需要拷貝至include資料夾以便編譯器進入main.cpp時能透過#include認識該函式庫會有什麼可用的函式以避免編譯器出現錯誤。


以上是main.cpp的內容,十分簡單的在main.cpp定義了一個main 函式以作為Entry Point ,main 函式裡則分別是使用了add_two_numbers以add_three_numbers兩個函式庫的函式,並輸出其結困果至終端機。
在文件最上方include了API.hpp以及API2.hpp,如前一篇文章所言,其用途是為了提醒進到main.cpp的編譯器有什麼函式名稱是可以過關的。


以上是CMakeLists.txt的全部內容

#include路徑心得分享

我個人Coding的環境主要是在用Visual Studio Code。Vscode有時會不認得Include的路徑但這不代表編譯器不認得,也不代表CMake不認得,兩者是獨立運行的。如果在CMakeLists.txt中加入include_directories以提醒CMake哪裡可以找到兩個頭文件的話,即使把#include "..\include\API.hpp" 改成了#include "API.hpp",一樣也能夠成功編譯,因為你已經透過了CMake告訴了編譯器哪裡也會有頭文件。但如果沒有的話,編譯器就會以include別人的那份頭文件來計算相對路徑。
而Vscode主要是靠C/C++的插件來檢查錯誤,與CMake是兩套獨立的系統,因此Vscode即使有錯誤,CMake也可以正常編譯,如果要消除其顯示的錯誤可以到File > Preferences > Setting > Search "Inlcude Path" > Add Item > ${workspaceFolder}\include 。
因此,在思考相對路徑時要以編譯器的角度去思考。

#動態庫進一步介紹

在編譯動態庫之前,動態庫與靜態庫的型式非常不同,因此必須要先詳解一下動態庫的知識。
靜態庫的副檔名分別是Windows上的.lib以及Unix-like的.a,其中的.a檔的a是Archive的意思,簡單來說便是一系列代碼的合集,也就是一堆.o檔案組合在一起的檔案,使用起來當然也與連結.o檔案的本身不會差太多,但動態庫卻完全不同。

動態庫是Microsoft發展出來的一種共享程式碼方案,也繼承了Microsoft一直以來的尿性,動態庫的.dll檔其本質與.exe的可執行檔的格式相同,也就是說所謂的動態庫其實本質就是一個輸入參數,並Return答案的程式。
最大的好處除了先前有提到的容量較小,還有就是記憶體使用也會更少,畢竟只有在被調用時動態庫才會被啟動。

#動態庫編譯

範例資料夾結構 : 


編寫動態庫時並不需要頭文件,因此只有一個add_two_value的函式,但請留意在函式定義前要加上__declspec(dllexport)的前綴。


以上是CMakeLists.txt的所有需要的內容,簡單的一句add_library。
但要把編譯靜態庫時的STATIC改成SHARED。 


最後再輸入CMake的指令,並檢查build/Debug資料夾中是否有.dll檔案成功被編出來。

#動態庫使用 

動態庫的使用上並不需要用到CMake,而是要在C++代碼中利用windows.h中的 LoadLibrary來讀取該.dll檔。並且用typedef先定義.dll檔中會有的函式,最後用GetProcAddress從.dll中Lookup其中的函式。

至於編譯的部分並不需要特別在CMakeLists.txt裡加筆,直接編譯main.cpp即可。





熱門文章