从零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