從零Makefile落地算法大項目,完整案例教程
轉自:從零Makefile落地算法大項目,完整案例教程
作者:手寫AI
前言
- 在這里,你能學到基于Makefile的正式大項目的使用方式和考慮,相信我,其實可以很簡單。而且寫一次到處用,新項目復制即可用
- 本教程帶你一步步完成,ppt很長是因為細,內容不多
- 相比cmake,Makefile更加輕量簡潔,侵入性低,掌控力強。語法更少更簡單
- 使用Makefile你可以進行更細粒度的掌控,雖然cmake簡化了這些,但是對于大型項目,細節的掌控是必須的
- 對于算法落地,我們會面臨各種庫包,理清楚非常有利于降低問題的發生
- 這也是高級算法工程師系列課程的基礎,貫穿后續課程的存在。后續會有CUDA編程、TensorRT、基于C++實現BP、流媒體等等
準備環境
- VSCode (Visual Studio Code),作為IDE
- 安裝VSCode的C++插件
- 準備Linux系統(Ubuntu),推薦使用VSCode的SSH插件遠程連接服務器(另一個電腦)進行開發。本地電腦可以是windows/mac
- 熟悉C++的基本語法,我們主講Makefile但是會有C++編碼部分(不多)
目錄
- g++指令介紹
- C++的編譯鏈接過程、編譯時、運行時介紹
- Makefile基本語法,依賴關系定義
- 基于Makefile的標準工程結構
- 基于Makefile實現的完整功能項目
- 分析程序依賴項,readelf、ldd
- 配置C++的調試功能
- 頭文件修改cpp自動編譯的處理方法
GITHUB項目地址(PPT、代碼均在這里)
https://github.com/shouxieai/makefile_tutorial_project
1. g++指令介紹
1.1 g++/gcc是什么,有什么區別
- g++和gcc都是gnu推出的cpp編譯器,時代不同
- g++和gcc都可以進行cpp編譯
- g++和gcc一樣,都屬于driver,即驅動編譯,他會調用cclplus/ccl/ld/as等指令實現編譯鏈接等工作,他們倆只是默認選項上的處理不同
- 這里我們采用g++而不是gcc
- g++ 等價于 gcc -xc++ -lstdc++ -shared-libgcc
- 參考:知乎問題
1.2 g++的編譯過程
4種情況,注意指令的大小寫很重要
- 預處理:
g++ -E main.cpp -o main.i
- 匯編:
g++ -S main.i -o main.s
- 編譯:
g++ -c main.s -o main.o
- 鏈接:
g++ main.o -o main.bin
g++可以允許跨過中間步驟,例如:
- g++ -S main.cpp -o main.s
- g++ main.s -o main.bin
- g++ main.cpp -o main.bin
- 結果是等價的
比較常用的是編譯-鏈接:
- 編譯,代碼編譯到二進制:g++ -c main.cpp -o main.o
- 鏈接,多個二進制鏈接成執行程序:g++ main.o -o main.bin
預處理指令效果:g++ -E main.cpp -o main.i
匯編指令效果:g++ -S main.i -o main.s
編譯指令效果:g++ -c main.s -o main.o
鏈接指令效果:g++ main.o -o out.bin
2. C++編譯鏈接 / 編譯時和運行時
2.1 C++編譯鏈接流程圖
2.2 C++的聲明和實現
2.3 C++的編譯過程-案例
2.3.1 代碼結構,main.cpp和test.cpp
2.3.2 main.cpp的匯編代碼
2.3.3 test.cpp的匯編代碼
2.3.4 兩者匯編代碼對比
- main.s里面沒有add函數的具體實現,只有call add操作
- add的具體實現在test.s里面
2.3.5 帶有命名空間時的名字編碼
2.4 C++編譯過程
2.5 C++鏈接過程
2.6 C++實際的鏈接過程
2.7 若add函數在動態庫,lib3rd.so中時
2.8 若add函數在靜態庫,libpkg.a中時
2.9 編譯鏈接成一個完整程序的過程
2.10 C++鏈接時,查找so文件、a文件方式的方式
g++ -lpkg,這里是小寫的L
2.11 C++運行時,查找so文件的方式
2.12 C++編譯時,頭文件的查找方式
- 這里是大寫的i,-lfolder
3. Makefile基礎
3.1 Makefile基礎-解決的問題是什么
- 編譯代碼是一個很耗時的事情尤其是代碼量大、CPU差時(邊緣端)
- 參考官方文檔,查看更多定義:http://www.gnu.org/software/make/manual/make.html
3.2 Makefile基礎-代碼域
3.3 Makefile基礎-語法
- 生成項可以沒有依賴項,那么如果該生成項文件不存在,command將永遠執行
3.2 依賴關系定義
- 第一次執行make a.o時,由于a.o不存在,執行了command
- 第二次執行make a.o時,由于a.cpp時間沒有比a.o新,打印a.o is up to date,不需要編譯
- 生成項和依賴項,從來都是當成文件來看待的
3.3 編譯和鏈接結合起來
- 定義好依賴后make out.bin后,會自動查找依賴關系,并自動按照順序執行command
- 這是makefile為我們解決的核心問題,剩下就是如何玩的更方便罷了。比如自動檢索a.cpp、b.cpp,自動定義a.o依賴a.cpp。等等
3.4 總結
- 變量賦值有4種方式var = 123, var := 123, var ?= 123, var += 123。其中var := 123常用,var += 123常用
- 取變量值有兩種,
$(var)
,${var}
。小括號大括號均可以 - 數據類型只有字符串和字符串數組,空格隔開表示多個元素
$(function arguments)
是調用make內置函數的方法,具體可以參考官方文檔的函數大全。但是常用的其實只有少數兩個即可- 依賴關系定義中,如果代碼修改時間比生成的更新/生成不存在時,command會執行。否則只會打印main.o is up to date。這是makefile解決的核心問題
- 依賴關系可以鏈式的定義,即b依賴a,c依賴b,而make會自動鏈式的查找并根據時間執行command
- command是shell指令,可以使用$(var)來將變量用到其中。前面加@表示執行執行時不打印原指令內容。否則默認打印指令后再執行指令
- make不寫具體生成名稱,則會選擇依賴關系中的第一項生成
還有問題
4. 基于Makefile的標準工程結構
4.1 Makefile工程結構
- 一個標準工程,我們做如下定義:
- 具有src目錄,存放我們的代碼,可能有多級,例如main.cpp,foo/foo.cpp等
- 具有workspace目錄,存放我們編譯后的可執行程序、資源、數據
- 具有objs目錄,存放由cpp編譯后得到的o文件等中間文件
- .vscode目錄,存放vscode的cpp配置,用于語法解析器。vscode的c++插件所使用。ctrl+shift+p后搜索c++,找到JSON那一項就是
- Makefile文件,當前工程的Makefile代碼
4.2 寫代碼
- 這里簡單定義了foo.hpp和foo.cpp,目的是鏈接為可執行程序后,可以執行
- ifndef是防止重復包含
4.3 解決多級目錄cpp檢索問題
4.4 替換src/為objs/,o文件放到objs中
4.5 定義依賴關系,通配
- objs/%.o和src/%.cpp代表了通配依賴關系,模式匹配,%相當于變量部分
4.6 為o文件創建目錄
4.6.1 編譯失敗,因為目錄不存在
- 原因是,試圖創建objs/foo/foo.o文件時失敗。因為objs/foo這個目錄不存在造成。對于高版本g++(例如9.0)不會報錯并為你創建objs/foo目錄。
- 因此我們需要創建objs/foo目錄,需要執行類似
mkdir -p dir($@)
,通過dir($@)
獲取其目錄后創建,這里的mkdir -p
指多級目錄也一并創建
4.6.2 使用mkdir -p $(dir $@)
獲取生成項目錄
4.7 鏈接所有o文件生成可執行程序
- 我們定義workspace/pro的生成,依賴自所有的o文件。pro是我們的可執行程序
4.8 完善一下Makefile
- 添加make pro,簡潔的編譯程序
- 添加make run,編譯后順便執行一下,注意: cd到workspace是為了讓運行程 序后的當前工作目錄在workspace中
- 添加make clean,清理編譯后的垃圾
- 添加.PHONY,讓我們作為指令存在的東西,不要被視作為文件。即make這東西時永遠執行command
4.9 完整版本的Makefile
4.10 可以愉快的玩耍了
4.11 修改一個cpp后觀察效果
- 對,這就是我們想要的,nice!
5. 基于Makefile實現的完整功能項目
- 這一份代碼,你可以點擊下載
5.1 Makefile工程-一個復雜的例子,實現http請求
-
實現的目的:
-
具有兩個依賴,openssl、libcurl
-
存在include、libs依賴
-
可以鍛煉一個完整的相對完善的工程案例。還可以鍛煉到代碼調試
-
實現的效果:
實現一個程序,可以從任何網站上下載東西
-
準備:
-
下載openssl:https://www.openssl.org/source/old/1.1.1/openssl-1.1.1j.tar.gz
這是用于實現加密通信的加密算法庫。用于訪問https開頭的鏈接
-
下載libcurl:https://curl.se/download/curl-7.78.0.tar.gz
這個是用于實現http/https的訪問操作。如果要訪問https,則依賴openssl
-
5.2 下載和編譯libcurl/openssl
- 創建build目錄,用于儲存下載后的文件,準備用來編譯
- 創建lean目錄,用于存放編譯后的結果,作為依賴項目錄
- 將下載后的.tar.gz放到 build目錄下,并解壓出來
5.3 編譯openssl
cd openssl-1.1.1j
./config --prefix=/data/sxai/makefile/make7/lean/openssl-1.1.1j
make all -j16 && make install -j16
./config
是配置并生成Makefile,指定install到/data/sxai/makefile/make7/lean/openssl-1.1.1j
目錄make all -j16 && make install -j16
這里-j16是同時16個線程執行操作,編譯后,執行安裝- 請把這里的lean目錄修改為你當前自己想放的位置
5.4 編譯libcurl
./configure --prefix=/data/sxai/makefile/make7/lean/curl7.78.0 \--with-openssl=/data/sxai/makefile/make7/lean/openssl-1.1.1j
make all -j16 && make install -j16
--prefix
同樣是為了設置安裝目錄,最后編譯好的curl放在哪里--with-openssl
指定剛才我們編譯安裝后的目錄./configure
同樣是為了配置curl生成Makefile文件- 執行
make all -j16
實現編譯 - 執行
make install -j16
實現安裝
5.5 編譯后結果
5.6 配置IntellSense和browse路徑
- 變量
${workspaceFolder}
代表了我們的當前目錄,即/data/sxai/makefile/make7
5.7 配置Makefile
5.7.1 第一步
5.7.2 第二步
- 好,我們齊活了。至此整個makefile已經非常完備了。該makefile可以通用了
給出代碼:
srcs := $(shell find src -name "*.cpp")
objs := $(srcs:.cpp=.o)
objs := $(objs:src/%=objs/%)
mks := $(objs:.o=.mk)include_paths := lean/curl7.78.0/include \lean/openssl-1.1.1j/includelibrary_paths := lean/curl7.78.0/lib \lean/openssl-1.1.1j/lib# 把library path給拼接為一個字符串,例如a b c => a:b:c
# 然后使得LD_LIBRARY_PATH=a:b:c
empty :=
library_path_export := $(subst $(empty) $(empty),:,$(library_paths))ld_librarys := curl ssl crypto# 把每一個頭文件路徑前面增加-I,庫文件路徑前面增加-L,鏈接選項前面加-l
run_paths := $(library_paths:%=-Wl,-rpath=%)
include_paths := $(include_paths:%=-I%)
library_paths := $(library_paths:%=-L%)
ld_librarys := $(ld_librarys:%=-l%)compile_flags := -std=c++11 -w -g -O0 $(include_paths)
link_flags := $(library_paths) $(ld_librarys) $(run_paths)# 所有的頭文件依賴產生的makefile文件,進行include
# -include表示如果有異常請不要打印出來
# 這里判斷,如果是clean指令,則不需要生成mk文件
ifneq ($(MAKECMDGOALS), clean)
-include $(mks)
endifobjs/%.o : src/%.cpp@echo 編譯$<,生成$@,目錄是:$(dir $@)@mkdir -p $(dir $@)@g++ -c $< -o $@ $(compile_flags)workspace/pro : $(objs)@echo 鏈接$@@g++ $^ -o $@ $(link_flags)objs/%.mk : src/%.cpp@echo 生成依賴$@@mkdir -p $(dir $@)@g++ -MM $< -MF $@ -MT $(@:.mk=.o) $(compile_flags)pro : workspace/pro@echo 編譯完成run : pro@cd workspace && ./proclean :@rm -rf workspace/pro objs.PHONY : pro run debug cleanexport LD_LIBRARY_PATH:=$(LD_LIBRARY_PATH):$(library_path_export)
5.8 寫代碼
5.8.1 第一段
- 這里寫一個download函數,接收url,然后返回下載后的數據
5.8.2 第二段
- 注意這里的地址換為:http://www.zifuture.com:1556/fs/sxai/2021/07/pro-18432c111ca44aa9bba49eab650f466c.jpg
- 實現一個main函數,調用download。給定地址是一個圖片下載好后儲存為文件
5.9 執行并觀察結果
- 文件下載成功,至此。整個http的訪問工程就達成了。你學會如何控制,頭文件、庫文件路徑了嗎?還有o文件存放工作目錄等
6. 分析程序依賴項
6.1 使用readelf -d workspace/pro分析
6.2 使用ldd workspace/pro分析
7. 配置C++的調試功能
7.1 配置task.json
- task.json是配置用來執行調試之前的編譯工作。即,按下F5,編譯程序,進入調試
7.2 配置launch.json
- 這個文件可用通過直接按下F5后自動產生,也可以手動敲哈
- 如果有參數,可以加到args中
- stopAtEntry表示啟動后就停止到main函數里邊
7.3 進行調試
- 好了,我們在main.cpp的29行這個文字左側點擊后后個紅點,作為斷點,然后按下F5鍵,看看會怎么樣
7.4 界面介紹
7.5 恭喜
- 到這里,恭喜你,已經掌握了如何使用
- Makefile在linux下開發的技能了!
- Congratulations!!!
8. 頭文件修改后自動編譯
- 代碼請到github上下載:https://github.com/shouxieai/makefile_tutorial_project
8.1 新建工程
- 我們有如下代碼。頭文件a.hpp中定義了Number 888
8.2 分析原因
- 原因:缺少a.o對hpp依賴關系的定義。makefile中沒有定義a.o : a.hpp,沒有要求編譯a.cpp需要檢查a.hpp的時間
- 解決方案?:直接增加a.o : a.cpp a.hpp嗎?是可以。強制要求 a.o生成時檢查a.hpp
8.3 解決方案
8.3.1 使用g++ -MM a.cpp -MF a.mk -MT prefix/a.o
生成makefile文件a.mk
- 由
g++ -MM a.cpp -MF a.mk -MT prefix/a.o
生成的makefile文件
8.3.2 通過include a.mk包含生成的文件,使其生效
- 我們使用
g++ -MM a.cpp -MF a.mk -MT a.o
- 為了使編譯后的a.mk生效,我們可以通過
include a.mk
包含進來
8.3.3 整合起來
注意,這里include a.mk
修改為-include a.mk
就不會提示報錯了
8.3.4 集成到項目中去
8.4 把代碼拆分出頭文件用于檢驗效果
8.5 至此,完整的Makefile工程搞定
- 謝謝
CPP工程模版,請參見:
https://github.com/shouxieai/cpp-proj-template