最近需要用c++写一些项目。以前c或者c++以及c#的主要使用范围都是:作业、小项目、刷题、面试考试,基本上都是单个文件或者单个项目就能解决的,也很少有链接外部库等比较工程的操作。但是现在要做开发就不能这样了,就得有体系。
一开始是打算在windows上做的,但是遇到诸多麻烦,所以最后还是老老实实在linux(比如ubuntu)上进行的开发,由于可以使用VMWare加NAT转发,所以可以在windows上远程连接,还是比较方便的。
找不到头文件
发生此问题时,环境位于windows。虽然最后还是没有在windows上开发,但是还是记录一下具体过程。
使用vscode需要配置对应的编译器,例如mingw32的gcc或者g++.
如果已经配置了但还是找不到,就顺藤摸瓜找到最底层的那个依赖的头文件是哪个:
- 如果是一个自己写的头文件,那说明include path有问题,修改vscode配置中的这一项。
- 如果是一个系统提供的头文件,那说明对应的依赖可能没安装。
比如我看到最终报错是pthread.h
没安装,如果是使用的Visual studio那就应该不会有这个问题,因为它应该安装时自己配了;但是如果使用vscode,使用自己下载的mingw,那么这个库可能没下载。打开MinGW Installer(它的exe名称是guimain.exe
),选择对应的pthread库,然后再在左上角的installation里面选择apply changes就安装好了。需要重启vscode,就能看到没有红色波浪线了。
总的来说,如果要在windows做长期的项目开发,还是Visual Studio这种比较重的IDE更好一点,java开发其实也是一样的,也得靠IDEA。
CMake与Make
这两个东西基本上绕不开。众所周知c++是一种编译型的语言,不像python之类的是一边解释一边执行。如果是使用Visual Studio之类的IDE,点一下小三角就能运行了,此时自然不用关心细节,并且这些都有自己的解决方案管理,比如Visual Studio的sln文件,或者Qt项目自己的等等。然而,vscode则没有这些。以本次做的TsFile项目为例,他是使用CMake来管理的。
gcc/g++
要编译c++文件,基本绕不开gcc/g++之类的编译器。他们会对文件进行“翻译”和“链接”,这里就不过多阐述原理了。但是一句一句指定应该编译哪些文件是很麻烦的,所以需要一些管理工具。
Make
可以使用Makefile来指定Make应该如何操作。如果是在windows,make这玩意一般是MinGW32-make或者其他之类的可执行文件,在linux就直接是make就行。make能够把gcc/g++的编译过程进行进一步梳理,这样就可以简化很多流程。但是,在Makefile中仍然还是需要指定o文件编译之类的过程,因此还是不够优雅。此时就需要CMake出场。
CMake
CMake实际上就是把编译过程进行了进一步管理。比如如下的文件就能很好的定义一个项目的编译:
# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(examples)
add_executable(examples examples.cpp)
这个代码实际上就是指定了要求什么CMake版本、项目的名字是什么、需要把什么文件编译为可执行文件,很简洁吧。而CMake在实际编译过程中,会在build
文件夹下自动生成Makefile
文件,具体的编译实际上是通过make
命令根据Makefile
文件进行编译的。
使用vscode调试
写代码,总不能不会调试吧。只会用命令行编译,然后运行可执行文件,那很可能无法定位bug位于哪里。在调试的时候,知道错误出现在哪一行代码、出错原因是什么,都是很重要的。既然不想用Visual Studio那么重的IDE,选择了vscode,那就得忍受手动配置这些玩意的痛苦。
首先,要解释一下C++插件和CMake插件是两个不同的玩意。
C++插件
这些插件主要是用于高亮语法之类的。对于单个文件,他们尚且还能配置成比较好用的模样,但是对于较大的项目就比较麻烦。比如launch.json配置的很多gdb调试任务,并不会关心所执行的文件是否为最新的。比如看看这段:
{
"version": "0.2.0",
"configurations": [
{
"name": "(gdb) examples.cpp启动",
"type": "cppdbg",
"request": "launch",
"program": "${workspaceFolder}/build/examples",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
},
{
"description": "将反汇编风格设置为 Intel",
"text": "-gdb-set disassembly-flavor intel",
"ignoreFailures": true
}
]
}
]
}
其中比较重要的其实就俩:"program"
字段指定了要用gdb运行哪个文件,"cwd"
指定了程序在哪个目录运行。乍一看他能调试,但是如果对源文件有修改,直接点小三角它可不会重新编译哦,而是还在使用旧文件进行调试,这能忍吗?
这个文件,c_cpp_properties.json
很重要,它配置了一些外部的头文件和源代码,以及使用的c++标准。如果头文件标红,就好好检查一下"includePath"
吧。
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**",
"/home/ubuntu/Documents/GitHub/tsfile/cpp/src",
"/home/ubuntu/Documents/GitHub/tsfile/cpp/third_party/antlr4-cpp-runtime-4/runtime/src/"
],
"defines": [],
"compilerPath": "/usr/bin/gcc",
"cStandard": "c17",
"cppStandard": "gnu++17",
"intelliSenseMode": "linux-gcc-x64"
}
],
"version": 4
}
CMake插件
说了这么多,还只是看起来好看,并不能实际优雅地运行。而CMake插件就能做到这一点:当你修改了某些代码,按一个按钮就会自动编译,然运行起来供你测试;而且如果出现了问题,还定定位到是源代码的哪个位置。
首先介绍的是右键配置的功能:右键一个CMakeLists.txt
,会显示“生成所有项目”和“配置所有项目”,这两个功能...emm还在研究这么用,总之很难用,建议不要碰。
刚刚不是说能够一键调试吗?这个入口比较隐蔽,看下面的状态栏(没错,这么常用的功能居然做在这么小的地方太可恶了)
好吧我猜你肯定看不清,它就是这么小,这是放大之后的图:
这里就是CMake插件的核心功能了。
- CMake:[Debug],这个按钮指的是编译类型,你可以选择Debug或者Release
- 工具包,这玩意就是g++配置什么的,一般不用管
- 生成,这玩意你点一下他就开始编译项目,应该和右键菜单的“生成所有项目”类似
- [all],这玩意是指生成的目标。
- all自然就是指的生成全部目标
- 如果你有一些子项目、模块什么的,可能会有子CMakeLists,可以单独生成对应的
- 小虫虫,就是最常用的调试按钮啦
- 点一下就进入调试了和c++插件的点小三角调试类似
- 不同之处在于,它在调试前,会先进行编译,保证源文件更改反映到程序上
- 小三角,就是直接运行可执行程序啦。其他注意事项类似调试。
- [examples],这个是项目名称
- 其他功能,例如运行CPack、运行工作流还没用过,学会了再回来填坑
重要注意事项
尽量不要项目套项目。比如TsFile项目是有一些样例的,但是它的examples
子项目直接就在tsfile/cpp
项目里面,如果你打开的文件夹不对,上述调试步骤很可能出现找不到源文件的问题。所以最好确保自己的vscode工作区就只有这一个项目,然后在CMakeLists.txt
以及c_cpp_properties.json
里面指定外部依赖。
交叉编译
没错,这是非常恶心的事情。。。交叉编译!!!虽然上面推荐了需要使用linux系统进行编译,比如使用ubuntu作为平时的开发环境,但是还不够哇!!!这是因为最终程序是需要在嵌入式硬件的arm架构上运行的,而无论是自己的windows还是ubuntu都是在x86_64上的,两者的指令集不同。所以,必须要使用交叉编译。
注意,交叉编译的实际过程都是在主机(host)完成的,此时还并没有把程序传输到嵌入式硬件。
准备工作
想要运行自己的项目?它依赖于TsFile。那么,就首先要交叉编译TsFile。此处,ChatGPT还是用处非常大,他给出了较为详实的步骤。需要如下准备:
- 工具链,toolchain。你需要这个工具包来完成该操作。他是运行在host的x86环境上的,但是输出的程序却是arm的。
- 配置cmake。你需要告诉cmake使用上述工具链进行编译,为此需要走超级长的弯路。
交叉编译工具链
工具链下载
在这里下载工具链:https://developer.arm.com/downloads/-/arm-gnu-toolchain-downloads
文件非常之多,这里需要解释一下应该下载哪个文件。需要注意以下几点:
- 学会读架构名称和操作系统名称。
- 知道host类型。也就是你用来做开发的平台类型。
- 知道target类型。也就是你目标程序运行的平台类型。
如下是开发平台名称(host类型):
开发平台名称/host类型 | 具体含义 |
Windows (mingw-w64-i686) | 32位windows操作系统 |
Windows (mingw-w64-x86_64) | 64位windows操作系统 |
x86_64 Linux | 使用x86架构的Linux系统 |
aarch64 Linux | 使用aarch64架构的Linux系统 |
macOS (x86_64) | 使用x86芯片的Mac |
macOS (Apple silicon) | 使用苹果芯片的Mac |
如下是目标运行平台的名称(target类型):
标运行平台的名称/target类型 | 具体含义 |
AArch32 bare-metal target (arm-none-eabi) | 没有操作系统支持的裸机编程目标。 |
AArch32 GNU/Linux target with hard float (arm-none-linux-gnueabihf) | 基于 GNU/Linux 系统的 ARM 目标,且支持硬件浮点运算(gnueabihf )。 |
AArch64 bare-metal target (aarch64-none-elf) | 64 位的裸机目标,不依赖任何操作系统。 |
AArch64 GNU/Linux target (aarch64-none-linux-gnu) | 64 位的 ARM 架构,运行 GNU/Linux 系统。 |
AArch64 GNU/Linux big-endian target (aarch64_be-none-linux-gnu) | 使用大端法的64 位的 ARM 架构,运行 GNU/Linux 系统。 |
所以最后我的选择是:arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf.tar.xz,其含义是:我的host是x86_64的Linux主机(Ubuntu),而目标是一个支持硬件浮点运算的GNU/Linux 系统。
如果不知道目标是什么样的,可以使用uname -a
进行查看或者使用cat /proc/cpuinfo
查看。
工具链安装
解压它,添加对应的执行路径即可。其实主要在CMake内配置,所以不加到path问题也不太大。注意下面的命令要换成自己的路径,加入到~/.bashrc
中。
export PATH=/home/ubuntu/software/arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf/bin:$PATH
配置CMake
在项目目录下新建toolchain-arm.cmake
,它的具体路径是:tsfile/cpp/toolchain-arm.cmake
,这个文件用来配置toolchain。
# toolchain-arm.cmake
# 配置交叉编译工具链,使用 ARM 编译器
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 自己下载的, 最终使用的是13.3.rel1
set(tools /home/ubuntu/software/arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf)
set(CMAKE_C_COMPILER ${tools}/bin/arm-none-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER ${tools}/bin/arm-none-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH ${tools}/lib)
include_directories(${tools}/include)
在所使用的cmake命令中通过参数指定所使用的工具链。这里我是编译TsFile项目,它有一个写好的自动build脚本。所以我可以直接修改build.sh
(最好是复制一份并且使用不同的build路径把x86编译和arm编译分开)。所使用的参数如下:
-DCMAKE_TOOLCHAIN_FILE=toolchain-arm.cmake
使用该参数之后就可以使用指定的工具链进行编译了。所以,我这里使用的是直接利用命令行编译,没有配置vscode脚本。其实也可以通过vscode的json配置文件指定cmake进行交叉编译。
交叉编译依赖
如上所述,交叉编译需要输出arm架构的结果,那么它自然也就需要一些arm架构的依赖。比如说你在一台ubuntu(x86)上交叉编译一个嵌入式设备(arm7)的可执行文件,而它依赖libuuid这个库,那么libuuid这个库就需要是arm版本的而不是x86版本的。
安装x86版本的uuid库很简单:sudo apt-get install uuid-dev
。但是arm版本的就需要自己下载编译才行了,非常坏!
在这里下载libuuid源码:libuuid 项目。
或者直接下载链接是:http://nchc.dl.sourceforge.net/project/libuuid/libuuid-1.0.3.tar.gz
下载好之后,先编译(make)再安装(make install)。具体命令如下:
./configure CC=/home/ubuntu/software/arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf/bin/arm-none-linux-gnueabihf-gcc --host=arm-linux --prefix=/home/ubuntu/software/arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf
make
make install
此处CC
指定的是gcc编译器路径,--host
指定了目标是arm架构的linux,而--prefix
指定了它的输出目录。它会在输出目录下输出lib
和include
两个文件夹。
然而,如果你只是复制了输出的lib
和include
两个文件夹到工具链目录下,很可能编译出错,会由ld
提示“没有那个文件或目录”。这其实就是没能找到文件。这其实和ld
程序寻找库地址的方式相关。
如下是我的交叉编译工具链目录,注意,省去了其他与该问题不太相关的文件夹:
/home/ubuntu/software/arm-gnu-toolchain-13.3.rel1-x86_64-arm-none-linux-gnueabihf/
├── 13.3.rel1-x86_64-arm-none-linux-gnueabihf-manifest.txt
├── arm-none-linux-gnueabihf
│ ├── bin
│ ├── include
│ ├── lib
│ └── libc
├── bin/
├── include
│ ├── gdb
│ └── uuid
├── lib
│ ├── bfd-plugins
│ ├── gcc
│ ├── libuuid.a
│ ├── libuuid.la
│ ├── libuuid.so -> libuuid.so.1.0.0
│ ├── libuuid.so.1 -> libuuid.so.1.0.0
│ ├── libuuid.so.1.0.0
│ └── pkgconfig
注意到问题所在了吗?工具链的根目录含有lib和include文件夹,而文件夹arm-none-linux-gnueabihf
下同样含有lib和include文件夹。犯错的原因就在于只把交叉编译的uuid相关文件复制到了工具链根目录下,而没有同时复制到文件夹arm-none-linux-gnueabihf
下。
链接库
某些时候,你可能需要链接一个外部库,例如sqlite3. 这类库通常有简便的安装方法。需要注意,使用sudo apt install sqlite3
只会安装该应用,但是不会安装依赖库,需要安装libsqlite3-dev
才能正常引用相关库。
在安装完毕之后,你可能发现VSCode中,相应的红色波浪线消失了,并且Ctrl+点击该文件能够跳转到该文件了,觉得大功告成了——然而编译失败了。这是因为你在使用CMake编译。
请时刻注意:VSCode的高亮表现和实际编译的过程是分离的、不相关的。VSCode配置的界面依赖于c_cpp_properties.json
以及全局配置之类的来找到对应的库文件,找到就能实现函数跳转等方便的功能——但这个和CMake的编译无关。换句话说,CMake编译时可能仍然找不到该文件,因此需要特别注意。
在遇到类似问题时,我搜索网上的相关问题,比如这篇,发现都是这么说的:在使用g++
编译器时加上-lsqlite3
参数,例如g++ main.cpp -lsqlite3
。换句话说,它使用一个参数指定了该项目依赖于sqlite3这个库。类似的,使用CMakeLists时也需要这样指定,只是形式不同:
target_link_libraries(main sqlite3)
这样就把sqlite3这个库链接到可执行程序main上了。
静态编译
有时,目标的嵌入式平台不包含所依赖的so链接文件,这很常见。然而,你又不想一个个看缺少哪些文件,甚至平台不支持ldd
命令以及file
命令,让你的排查很困难。此时,你可以使用静态编译。静态编译的原理非常简单:输出的可执行文件包含了所有依赖的库。
在CMAKELists中使用如下命令告诉编译器调用g++时加上-static
参数,达到静态编译的目的:
# 强制使用静态链接
set(CMAKE_EXE_LINKER_FLAGS "-static")
注意,这样编译出的文件大小会大非常多。一个简单的helloworld程序使用动态编译,大小可能只有100KB左右;而使用静态编译,其大小可以达到10MB,翻了一百倍。
CMakeLists编写
编写时有一点需要特别注意,那就是调用顺序。project命令会自动寻找编译器,因此,请务必确保toolchain的指定位于该命令之前,否则会发现使用了错误的g++导致不能正常编译。
此处列出一些常用命令,以及推荐的执行顺序顺序。
1. project 命令
project()
命令用于设置 CMake 项目的基本信息,如项目的名称、版本、支持的编译语言等。这是 CMake 配置过程中必须要调用的命令,通常是 CMake 文件的第一条命令之一。但是,类似于交叉编译的情景,指定工具链应该在project命令之前。
2. set 命令
set()
用于设置 CMake 变量的值。这些变量可以是编译器的路径、库的路径、选项等。
set(CMAKE_TOOLCHAIN_FILE ${CMAKE_SOURCE_DIR}/toolchain-arm.cmake)
- 此处
CMAKE_TOOLCHAIN_FILE
是 CMake 的一个标准变量,它指定了工具链文件的路径。 - 这个文件告诉 CMake 如何配置交叉编译环境,包括使用哪个编译器、链接器等。
- 如果需要交叉编译,必须在
project()
之前或至少在编译器选择之前设置工具链文件。
3. include_directories 命令
include_directories()
用于添加额外的头文件目录到 CMake 项目的编译器搜索路径中。CMake 会将此目录添加到编译器的 -I
选项中,确保编译时可以找到你指定的头文件。在交叉编译时,通常会使用工具链的头文件,因此这条命令对确保工具链工作至关重要。
- 在 CMake 中,通常会把系统和库的头文件目录放在
include_directories()
中。 include_directories()
应该尽量避免放在add_executable()
或target_link_libraries()
后面,因为这些命令才会进行实际的编译链接。
4. find_library 命令
find_library()
用于搜索库文件,并将其路径存储到变量中。
find_library(my_tsfile_lib NAMES tsfile PATHS ${SDK_LIB_DIR_RELEASE} NO_DEFAULT_PATH REQUIRED)
my_tsfile_lib
是一个变量,存储找到的库的路径。NAMES tsfile
表示要查找名为tsfile
的库。PATHS ${SDK_LIB_DIR_RELEASE}
指定搜索库的路径。NO_DEFAULT_PATH
表示不要使用 CMake 默认的库搜索路径。REQUIRED
表示如果找不到库文件,CMake 将报错。
find_library()
应该放在链接库之前,确保库路径被正确找到。
5. add_subdirectory 命令
add_subdirectory()
用于将一个子目录添加到当前项目中,并且会在该子目录中执行 CMake 配置。它们允许你组织项目代码,将大型项目分成多个模块或子项目。它可以让你将项目划分为多个子模块或子目录,进行独立管理。
6. add_executable 命令
add_executable()
用于定义一个可执行文件,并指定源文件。例如:add_executable(main main.cpp)
. 这个命令定义了一个名为 main
的可执行文件,源文件是 main.cpp
。注意,add_executable()
应该在定义可执行文件之前进行。如果需要链接外部库,必须在这之后使用 target_link_libraries()
。
7. target_link_libraries 命令
target_link_libraries()
用于将库文件链接到指定的目标(如可执行文件或库)。
target_link_libraries(main ${my_tsfile_lib})
target_link_libraries(main cpp_examples_obj multi_flush_exp_obj util_obj)
target_link_libraries(main sqlite3_exp_obj sqlite3)
- 第一个命令链接了外部的
tsfile
库。 - 第二个命令链接了
cpp_examples_obj
、multi_flush_exp_obj
和util_obj
目标,它们是在add_subdirectory()
添加的子项目中定义的目标。 - 第三个命令链接了
sqlite3
相关的库。
合理的调用顺序
为了避免此前遇到的编译器未正确设置问题,推荐按照如下顺序来组织 CMakeLists.txt
:
- 设置工具链和交叉编译器(如果需要): 这应该是 CMake 配置中的第一步,确保 CMake 在任何其他配置或编译操作之前已经了解如何使用交叉编译器。
- 调用
project()
命令: 在设置工具链后,调用project()
来设置项目的基本信息。 - 包含目录和库路径设置: 使用
include_directories()
和find_library()
来指定头文件和库文件路径。 - 添加子目录: 使用
add_subdirectory()
来将子项目添加到当前构建中。 - 创建可执行文件: 使用
add_executable()
创建可执行文件。 - 链接库: 使用
target_link_libraries()
将所需的库文件链接到目标可执行文件。
Comments NOTHING