SRS4.0源码分析-CMake - 弦外之音

/ 0评 / 0

SRS原理》上架了,深度剖析 SRS 源代码,访问地址:srs.xianwaizhiyin.net


本文采用的 SRS 版本是 4.0-b8 , 下载地址:github


SRS4.0源码分析-调试环境搭建》 讲了 SRS 在 Clion 里面的调试,本文主要讲解 srs-4.0-b8\trunk\ide\srs_clion目录下 的 CMakeLists.txt 文件的编译逻辑。

CMake 是对 makefile 的一种封装, CMake 最后会生成 makefile 文件,再执行 make 命令。

makefile 可以看 左耳朵耗子的《跟我一起写Makefile》

CMake 能找到的简洁的教程比较少,我之前看的是 CMake 官方文档《CMake Practice》 ,提取码:w1ks 。

《CMake Practice》 这本手册好像是未完成的。

CMake 的语法庞大而且复杂。说实话,我也不熟悉 CMake 的大部分语法,那我是如何分析 SRS 里面的 CMake 的逻辑呢?

靠搜索引擎,我对 Makefile 比较熟悉,所以借助搜索引擎,就能搞懂 SRS 里面的 CMake 的逻辑。

CMake 的语法庞大而且复杂。所以我个人建议,不需要看完他的官网文档语法,你用到去搜索就行。



开始分析 CMakeLists.txt 代码。

# 1 ~ 2 行
cmake_minimum_required(VERSION 2.8.12)
project(srs CXX)

上面的代码 规定了 CMake 的最低版本 是 2.8.12,然后 project(srs CXX) 把 项目 命名为 srs,同时指定是 C++ 项目,这个 CXX 就代表 C++,应该是因为 ++ 是特殊字符,所以换成了 xx 表示。

指定 CXX 有什么作用?指定了 CXXCMake 命令生成的 Makefile 就会使用 g++ 作为编译器。

同理,如果是 project(srs C) ,那就是使用 C 语言的编译器 gcc 来编译代码,所以这句 CMake 的代码是指定编译器的。

所以 CXX 是指定使用 C++ 的编译器,C 是指定使用 C语言的编译器,那这些编译器是在哪里指定的呢?我们知道C语言的编译器有多种的,gccclang 都可以编译 C语言代码。

Clion 指定 toolchain (编译工具链)的方法如下:

从上图可以看到, 指定的 C 跟 C++ 语言编辑器是自动匹配了,在 Ubuntu 下应该是用 whereis gccwhereis g++ 来确定路径,如果你想指定使用某个版本的 g++ 来编译项目,新建一个 Toolchains ,填上 g++ 的路径即可。

从上图 可以看到 默认使用的 调试工具是 gdb

所以这些 集成工具clionqt creatorVS2019 等等,都只是把 gccg++makegdb 等命令 集成,然后做界面化而已。

这些集成工具使用到的 命令 都是可以配置的,还有选项也是可以配置的,例如 gcc 的选项,都是有地方配置的。

理解了这个本质,后续无论使用哪种编译工具,都能很快上手。


# 5 ~ 8 行
execute_process(
        COMMAND bash -c "cd ${PROJECT_SOURCE_DIR}/../../ && pwd"
        OUTPUT_VARIABLE SRS_DIR
)

上面的 execute_process 就是执行系统命令,用 cd 切换 路径,然后 pwd 输出 路径内容 赋值给 SRS_DIR 变量。

execute_process 的 用法,请看 《cmake的命令execute_process


接下来的分析会用到一些 CMake 的调试技巧,先讲解一下:

1,如何让 CMake 退出编译,达到打断点的效果。

解答:message(FATAL_ERROR "stop exit") ,用 FATAL_ERROR 来退出 CMake 解析后面的代码。

2,CMake 如何打印变量。

SET(USER_KEY, "Hello World")
MESSAGE( STATUS "this var key = ${USER_KEY}.")

解答:用 message 函数即可打印变量。


综上,我们使用 以上的技巧调试一下 SRS 的 CMakeLists.txt 文件,代码如下:

cmake_minimum_required(VERSION 2.8.12)
project(srs CXX)
​
###########################################################
execute_process(
        COMMAND bash -c "cd ${PROJECT_SOURCE_DIR}/../../ && pwd"
        OUTPUT_VARIABLE SRS_DIR
)
string(STRIP ${SRS_DIR} SRS_DIR)
message("SRS home is ${SRS_DIR}")
# 调试代码
MESSAGE(STATUS "SRS_DIR = ${SRS_DIR}")
message(FATAL_ERROR "stop exit")
# 后续代码不会执行 ,省略

上面的代码 打印了一下 SRS_DIR 变量。执行以下命令开始 编译。

cd ~/Documents/srs-4.0-b8/trunk/ide/srs_clion
cmake ./

可以看到,正常打印出来 SRS_DIR 变量,并且退出了。


调试技巧讲完了,继续分析 CMakeLists.txt 文件 的代码。

# 13 ~ 15 行
# Start to configure SRS with jobs of number of CPUs.
include(ProcessorCount)
ProcessorCount(JOBS)

ProcessorCountCMake 的内部函数,获取机器有多少个CPU ,然后赋值给 JOBS 变量。在这里把 JOBS 打印出来可以看到是 16,我是16核CPU

推荐阅读《ProcessorCount 用法》


# 17 ~ 22 行
# We should always configure SRS for switching between branches.
IF (${CMAKE_SYSTEM_NAME} MATCHES "Darwin")
    EXEC_PROGRAM("cd ${SRS_DIR} && ./configure --osx --jobs=${JOBS}")
ELSE ()
    EXEC_PROGRAM("cd ${SRS_DIR} && ./configure --jobs=${JOBS}")
ENDIF ()

上面的代码直接 执行 ./configureconfigure 是 SRS 源码的一个 shell 脚本。

EXEC_PROGRAMexecute_process 是类似的函数,都是执行系统命令。


# 24 ~ 38 行
set(DEPS_LIBS ${SRS_DIR}/objs/st/libst.a
        ${SRS_DIR}/objs/openssl/lib/libssl.a
        ${SRS_DIR}/objs/openssl/lib/libcrypto.a
        ${SRS_DIR}/objs/srtp2/lib/libsrtp2.a
        ${SRS_DIR}/objs/ffmpeg/lib/libavcodec.a
        ${SRS_DIR}/objs/ffmpeg/lib/libavutil.a
        ${SRS_DIR}/objs/opus/lib/libopus.a
        ${SRS_DIR}/objs/ffmpeg/lib/libswresample.a)
foreach(DEPS_LIB ${DEPS_LIBS})
    IF (NOT EXISTS ${DEPS_LIB})
        MESSAGE(FATAL_ERROR "${DEPS_LIB} not found")
    ELSE ()
        MESSAGE("${DEPS_LIB} is ok")
    ENDIF ()
endforeach()

上面声明了一个 数组 变量 DEPS_LIBS,循环检测 这些 libst.alibssl.axxx.a 静态库是否存在,也就是检测之前的 configure 执行有没问题。

这些静态库 是在 configure SHELL 脚本里面生成的,为了避免本文过长, configure 的分析在 这篇文章《SRS4.0源码分析-configure》。

这里的情况跟 ffmpeg 的编译不太一样,通常情况,configure 不会做生成静态库之类的操作,configure 通常只做编译前的一些准备,例如检查 各个命令的版本号,一些头文件是否没问题等等。

但是 SRS 的 configure 生成了 一些外部静态库,这是一种简化项目编译的做法,如果让别人编译好静态库,再引入到SRS。像 ffmpeg 那样引入 libx264 ,会比较麻烦,不如把外部库的源码集成到 SRS,然后在 configure 里面直接 编译好静态库。


# 41 ~ 52 行
# Setup SRS project
INCLUDE_DIRECTORIES(${SRS_DIR}/objs
        ${SRS_DIR}/objs/st
        ${SRS_DIR}/objs/openssl/include
        ${SRS_DIR}/objs/srtp2/include
        ${SRS_DIR}/objs/opus/include
        ${SRS_DIR}/objs/ffmpeg/include
        ${SRS_DIR}/src/core
        ${SRS_DIR}/src/kernel
        ${SRS_DIR}/src/protocol
        ${SRS_DIR}/src/app
        ${SRS_DIR}/src/service)

CMakeINCLUDE_DIRECTORIES 函数 实际上就是同时指定 g++ 的编译选项 -Idir-Ldir 。g++ 编译的时候,搜索头文件或者库文件就会从上面这些目录找。

INCLUDE_DIRECTORIES 会翻译成 -Idir-Ldir


# 54 ~ 58 行
set(SOURCE_FILES ${SRS_DIR}/src/main/srs_main_server.cpp)
AUX_SOURCE_DIRECTORY(${SRS_DIR}/src/core SOURCE_FILES)
AUX_SOURCE_DIRECTORY(${SRS_DIR}/src/kernel SOURCE_FILES)
AUX_SOURCE_DIRECTORY(${SRS_DIR}/src/protocol SOURCE_FILES)
AUX_SOURCE_DIRECTORY(${SRS_DIR}/src/app SOURCE_FILES)

从上面的代码 可以看出 srs_main_server.cpp 应该是入口函数,main() 函数肯定在 srs_main_server.cpp 里面。

AUX_SOURCE_DIRECTORY 函数是干嘛的,我也不知道,这时候要借助搜索引擎。

参考 《aux_source_directory 用法》

搜索得知知道 AUX_SOURCE_DIRECTORY 就是遍历 ${SRS_DIR}/src/core 目录下的文件名,全部都加进去 SOURCE_FILES 变量里面


# 60 行
ADD_DEFINITIONS("-g -O0")

上面 ADD_DEFINITIONS 函数就是往编译器 g++ 加选项,会翻译 成 g++ -g -o0

-g 的作用是开启 编译调试,编译的时候会产生调试信息 给 gdb 使用。

-o0 的 o 是 optimize 的缩写,也就是优化,这里是设置编译优化的,设置 0 就是不优化,因为有时候如果优化了,gdb 调试代码的时候,跳转会很奇怪。

推荐阅读 《gcc debug选项》


# 62 ~ 65 行
ADD_EXECUTABLE(srs ${SOURCE_FILES})
TARGET_LINK_LIBRARIES(srs dl)
TARGET_LINK_LIBRARIES(srs ${DEPS_LIBS})
TARGET_LINK_LIBRARIES(srs -ldl -pthread)

上面的代码,是 生成 srs 执行文件的,可以看到 把 SOURCE_FILES 里所有的文件都编译进去。翻译成 编译命令如下:

g++ -o srs ${SOURCE_FILES}

SOURCE_FILES 变量是所有的 cpp 文件。但是这样还不行,还需要指定依赖哪些库,所以后面用了 TARGET_LINK_LIBRARIES 函数 来指定库依赖。

TARGET_LINK_LIBRARIES(srs dl) 应该是作者写多了,这句代码注释掉也能编译成功。

g++ 编译的时候指定外部静态库,有几种写法。

#第一种
g++ main.o libmyMath.a -o main

第一种是直接把 libmyMath.a 静态库的文件名写全。

#第二种
g++ main.o -o main -static -lmyMath -L ./

第二种是 用 -l 选项 后面带 库的部分文件名,没有 lib 前缀。然后 g++ 就会在相关的目录搜索 libmyMath.a 文件,把它编译进来。


所以详解分析 上面的 TARGET_LINK_LIBRARIES 相关代码

TARGET_LINK_LIBRARIES(srs ${DEPS_LIBS})

上面这行代码,DEPS_LIBS 是所有 静态库的文件名全集。所以翻译成 g++ 命令,是第一种写法。


TARGET_LINK_LIBRARIES(srs -ldl -pthread)

上面的 -ldl,前面带了 -l ,所以肯定有个文件名叫 libdl.a

果然,搜索发现。

/usr/lib/x86_64-linux-gnu/libdl.a
/usr/lib/x86_64-linux-gnu/libdl.so

如果没指定的话,应该默认是以动态库的方式编译加载 dl 库。

后面还有一个 -pthread,这个前面没有 -l,这个其实不是 g++ 的选项,而是 CMake 自创的语法,这个 -pthread 会被 cmake 命令翻译成 -lpthread 或者 pthread.lib 来适应不同的平台。pthread.lib 是windows 平台的库。

因为我的电脑是 ubuntu,所以 TARGET_LINK_LIBRARIES(srs -ldl -lpthread)TARGET_LINK_LIBRARIES(srs -ldl -pthread) 是一样的。


综上所述,通过这些 CMake 代码规则,就编译出了 srs 可执行文件,然后 Clion 就会调 GDB 来调试 srs

接下来看一些 CLion 执行 cmake 的过程,如下图:。

Clion 在 Load CMakeLists.txt 的时候会执行了以下命令,为 执行 cmake 做准备。

# 切换到 CMakeLists.txt 的目录 
cd /home/ubuntu/Documents/srs-4.0-b8/trunk/ide/srs_clion
# 创建 debug 编译目录
mkdir cmake-build-debug
# 进入 debug 编译目录
cd cmake-build-debug

接下来才是真正的 cmake 操作。

# 执行 cmake
/home/ubuntu/Documents/clion-2021.3.3/bin/cmake/linux/bin/cmake -DCMAKE_BUILD_TYPE=Debug -DCMAKE_MAKE_PROGRAM=/home/ubuntu/Documents/clion-2021.3.3/bin/ninja/linux/ninja -G Ninja /home/ubuntu/Documents/srs-4.0-b8/trunk/ide/srs_clion

从上面的命令可以看出 ,clion 指定了 ninja 来替换 make 命令, ninja 可以理解为 跟 make 一样的东西。

cmake 命令执行完之后 会生成 makefile 文件,在 clion 里面被替换成 ninja 后缀的文件了,然后 clion 会执行 ninja 编译生成 srs 可执行文件。

最后 生成 的 srs 可执行文件 在 cmake-build-debug 目录里面,如图:

最后,Cliongdb 来调试 这个 srs 可执行文件就行。


扩展知识补充:

1,cmake 命令执行的时候,会输出一些信息,例如 CXX 的版本,如下图:

上面的输出 实际上 就是 执行 g++ --version 的结果,如图:

2,如果不小心 误删了 cmake-build-debug 目录,这个目录 是Clion load CMakeLists.txt 时候创建的,误删了,点 debug 按钮就会报错,报错如下:

这个时候只需要 再执行一个 File -> Reload CMake Project 就好,如图:


相关阅读:

1,GCC 手册

2,GCC 官网手册


©版权所属:知识星球:弦外之音,QQ:2338195090。

由于笔者的水平有限, 加之编写的同时还要参与开发工作,文中难免会出现一些错误或者不准确的地方,恳请读者批评指正。如果读者有任何宝贵意见,可以加我微信 Loken1。

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注