C++开发(三)

C++开发(三)

最近的确学习到开发的知识。

Cmake串

可以在一个项目中使用多个CMake文件(CMakeLists.txt),在模块化的项目中非常常见。在项目的最顶层编写一个CMakeLists.txt,在其中可以通过add_subdirectory()命令去添加包含了子CMakeLists.txt的子模块目录。这样,在主目录执行cmake时,cmake会自动再跑到子目录接着执行对应目录的CMakeLists.txt。子目录如果还有子目录,则递归执行。

在递归的过程中,Cmake的环境得到了保持,比如上层增加的头文件目录include_directories在下层中还是会同样include。主CMakeLists.txt也可以不设置具体目标文件,而只是最一些全局配置,比如C++标准等。

1
2
3
4
5
6
7
8
9
10
project_root/
├── CMakeLists.txt # 顶层 CMake 文件
├── include/
│ ├── hello.h
├── cpp/
│ ├── CMakeLists.txt # cpp 子目录的 CMake 文件
│ ├── hello.cpp
│ └── tests/
│ ├── CMakeLists.txt # tests 子目录的 CMake 文件
│ └── test_hello.cpp

Homebrew安装

Python由于运行完全由解释器决定,包就是一个个.py文件,所以它的包管理容易一点。可以一个Python解释器版本对应一组.py文件,然后把这个解释器和包都放到一起就行了。其实这就是Anaconda做环境管理的思路。当然,现在还有项目驱动的包管理方法, 类似于Node管理库文件的方式,不再是一个解释器配一套库,而是一个项目配一套自己的库,Julia应该也是这样做的。

对于C/C++这种编译型语言,全局(只)有一套工具链。而且包不是Python那种动态型的,导入.py文件就可以运行了。而是需要提前编译成库文件,否则要不每次调用包写自己的代码,还需要把大包也一起编译了才能运行,这个繁琐程度是受不了的,而且也是没必要的。因为这些包的代码都是不变的,他们只是给我们自己的程序提供了些方便的函数或者类接口。这个时候,我们需要提前把他们编译成库文件,到时候我们自己的项目可以直接链接这些库文件,不需要重新编译这些大包。另外,我们在写程序的时候,因为编译器一次只能编译一个源文件,因此我们还需要这些大包中头文件的接口声明,方便编译器知道怎么分配空间一类的。因此安装一个C/C++的包,实际上就是两块,一块是头文件,这使得编译器知道怎么编译我们调用了大包的源文件。另一个就是我们要把大包的源文件编译成库文件,这样链接器能把库文件串到我们自己的项目上。

在Win里,没有一个自带的包管理工具。对于MacOS,也没有一个系统自带的包管理工具,但是开源的Homebrew已经成为了MacOS的标准了。Intel芯片的Mac和M芯片的Mac下Homebrew的默认目录是不同的,但是Apple逐渐就全是M芯片了,这里就只看看M芯片吧,反正也差不多。

Homebrew所谓的包管理,实际上和Anaconda也差不多,只不过它并不是解释器依赖的envs目录,而是在一个全局目录做包管理。Homebrew默认将包安装到/opt/homebrew目录下,这个文件包含了Homebrew的所有文件和软件包。Homebrew使用所谓的Formula(配方)来描述如何安装软件包。每个Formula是一个Ruby脚本,包含了从下载源代码、编译到安装软件包的完整过程。Formula存储在Github上的Homebrew仓库中,当运行brew install时,Homebrew会从这些仓库中获取相应的Formula。什么是包?就是一堆文件。

/opt/homebrew/Cellar(地下室、地窖)是Homebrew存储包的主要目录。每个软件包都会有一个子目录,并且按照版本号进一步分类。比如,安装的GoogleTest可能位于/opt/homebrew/Cellar/googletest/1.11.0。每个版本的目录包含了该软件包的所有文件,包括可执行文件、库文件、头文件等。/opt/homebrew/opt提供用于指向Cellar中某个软件包版本的符号链接。这使得我们总能够通过opt路径访问到当前的“激活”版本呢,而无需关心具体的版本号。

我们在安装Homebrew时,会把Homebrew的可执行文件路径添加到我们的$PATH路径中。Homebrew通过brew link命令,将其所安装的各大包的可执行文件都连接到这个路径,这样我们就可以通过命令行直接使用这些大包的可执行文件,而不需要手动把他们每个的bin路径都粘到$PATH中。此外,当安装某些库时,Homebrew可能会设置LDFLAGSCPPFLAGS环境变量,前一个用于指定连接时选项,后一个用于指定编译时选项。

另外呢,当通过Homebrew安装某些软件包时,Homebrew还可能会从源码编译这些软件,这也是根据Formula的指示编译安装的。对于常用的软件包,Homebrew也会提供预编译的二进制包,称为bottles。这些包下载后直接解压即可使用,无须机器再编译了。Homebrew源码编译具体来说,工具链的选择以及编译出的库文件的位置取决于Homebrew的配置和包的具体Formula。

Homebrew默认使用MacOS提供的编译器工具链,即Xcode中的clangclang++ld,以及make和其他相关工具。Homebrew也允许自定义工具链,可以通过环境变量如CCCXXLD等来指定不同的编译器或链接器。某些Formula中可能还指定了特定的工具链或者编译器选项,以确保兼容性或性能。例如某些包可能强制使用clanggcc,以确保与其他库或工具的一致性。

编译出来的库文件通常会放在Homebrew的地窖目录下,对应于具体包的子目录中。同时,为了方便使用,Homebrew会在/opt/homebrew/opt目录下创建相应的符号链接,指向Cellar目录中的实际文件。通常,编译出来的库文件会以.a(静态库)或.dylib(动态库)形式存放在于lib子目录下。而头文件会放置在include子目录下,供其他项目引用。Homebrew也会把大包的头文件、编译出来的可执行文件(如果有)、以及库文件,分别放到Homebrew下的includebin、以及lib目录中。我们也可以指定Homebrew的安装选项,比如在brew install安装的时候,--build-from-source会强制Homebrew从源码编译,而不是使用预编译的bottle二进制包;--cc=gcc-11会强制Homebrew使用指定的编译器。不同编译器生成的代码可能会存在不兼容,如果用clang编译一个库文件,然后用gcc编译自己的项目并链接到这个库,就可能会遇到API不兼容的问题导致报错。因此,依赖库的编译和项目的编译最好都采用相同的工具链。

另外,sbin目录,在各种情景下,一般都是系统管理员的二进制文件目录,通常包含那些普通用户不需要直接执行的系统管理工具或守护进程(daemon)。这些工具可能需要管理员权限才能运行,因此通常位于sbin而非bin目录中。Homebrew安装的软件包如果包含系统管理工具,可能会把可执行文件放到sbin目录下而不是bin目录。例如,网络服务管理工具或系统配置工具等,通常会安装在sbin目录中,以避免普通用户无意中执行这些命令。

Anaconda路径

Anaconda的环境中也有很多长得很像的库文件目录,其中

Lib目录,这是Windows平台的特有目录。主要用于存放Python的标准库和由condapip安装的纯Python包。标准库模块直接放到Lib目录中,site-packages子目录放置通过pipconda安装的第三方Python包。Unix系统下存在lib/pythonX.y目录下。

Library目录,这是Windows平台特有的目录,主要用于存放非Python的库和工具。它类似于Unix系统中的/usr/local目录。这个目录主要用于在Anaconda环境中处理非Python的依赖,特别是那些需要与C/C++代码交互的库。Library目录下有三个子目录:

include存放C/C++头文件。

lib存放C/C++静态库和动态库。

bin存放可执行文件和动态库。这个目录在Unix系统上有,在Windows平台变成了Scripts目录。

libs也是Windows平台特有的目录,用于存放Python解释器的库文件,如python38.lib。它主要用于链接Python解释器的库文件,以及如果需要在C/C++代码中嵌入Python解释器或者使用Python/C API,这个库会被用到。

各平台工具链

Linux:GCC(GNU Compiler Collection),Linux系统最常用的编译器,其中gcc(C编译器)、g++(C++)编译器、gfortran(Fortran)编译器;Clang:基于LLVM的编译器,clang(C/C++编译器)、clang++(C++编译器);GDB(GNU Debugger)调试器gdb

Mac:Clang基于LLVM的编译器,LLDB基于LLVM的调试器。

Win:MSVC(Microsoft Visual C++),cl编译器,link连接器;MinGW-w64,GCC的Windows版本;Clang,基于LLVM的编译器。

总结:

Linux:主要使用 GCC 或 Clang 作为编译器,搭配 Make、CMake 等构建工具,GDB 作为调试器。APT 和 Yum 是常见的包管理器。

macOS:默认使用 Clang 编译器,配合 Xcode 提供的工具链。Homebrew 是常用的包管理器,LLDB 是默认的调试器。

Windows:MSVC 是最常用的编译器和工具链,Visual Studio 提供全面的开发环境。MinGW 和 Clang 也被广泛使用。Chocolatey 和 vcpkg 是主要的包管理器。

另外,大公司Intel为它的硬件提供了一套Intel编译器套件(Intel one API Toolkits),这些编译器以高性能和对 Intel 硬件的优化著称,广泛应用于科学计算、工程仿真、高性能计算(HPC)等领域。

头文件引用

在C++中,""<>是用于包含头文件的两种不同的方式,它们之间的主要区别是编译器搜索头文件的路径顺序。#include "header.h"通常用于包含项目中自定义的头文件。编译器首先在包含当前源文件的目录中搜索该头文件,如果在当前目录中未找到,则继续在标准库的包含路径中搜索。#include <header.h>用于包含标准库或第三方库的头文件,编译器直接在标准库的包含路径中搜索该头文件,如果未找到,继续在其他指定的包含路径中查找。

多语言项目配置

现在手头的项目,既有Python、又有Cython、还有C++/Pybind11,需要规划出合理的目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
3DPhononMC/
├── .github/ # GitHub 配置和工作流
├── _docs_source/ # Sphinx文档源文件
├── _ipynb/ # Jupyter Notebooks 或其他交互式文档
├── _archive/ # 归档旧代码或参考资料
├── build/ # 构建输出目录(通常添加到 .gitignore)
├── database/ # 数据库或数据相关文件
├── docs/ # 文档生成输出目录
├── examples/ # 示例代码或教程
├── phononmc/ # Python代码
│ ├── __init__.py
│ ├── python_module.py # Python模块
│ ├── cython/ # Cython代码
│ │ ├── __init__.py
│ │ ├── cython_module1.pyx
│ │ └── cython_module2.pyx
│ └── utils/ # 辅助Python模块(如有需要)
│ └── helpers.py

├── cpp/ # C++/Pybind11代码
│ ├── CMakeLists.txt # C++模块的CMake文件
│ ├── hello.cpp # 主要 C++ 逻辑实现
│ ├── hello_wrapper.cpp # Python 接口实现
│ ├── include/ # 头文件目录
│ │ └── hello.h
│ └── tests/ # C++的测试代码目录
│ ├── CMakeLists.txt # GoogleTest的CMake文件
│ └── test_hello.cpp
├── tests/ # Python 测试代码目录
│ └── test_python.py # Python 测试代码
├── CMakeLists.txt # 顶层CMake文件
├── README.md # 项目简介文档
├── Makefile # Sphinx文档构建Makefile
├── setup.py # Python包安装配置文件
└── .gitignore # Git项目忽略文件

清晰的分离:通过将Python、Cython、C++/Pybind11代码放在各自独立的目录中,确保每个部分的代码和构建配置都能独立维护,减少耦合。

Cython目录独立:将Cython代码单独放在phononmc/cython/目录中,可以更明确地表明哪些部分是使用Cython实现的,有助于开发和调试。

头文件与实现分离:在cpp目录中,将头文件放在include/目录中,可以保持代码结构的清晰,并遵循良好的C++代码实践。

模块化测试:在cpp/tests/目录中管理所有C++测试代码,可以确保测试代码与实现代码分离,并简化测试的构建和执行流程。

逐步转换为 C++:随着项目的发展,你可以逐步将 phononmc/ 中的逻辑迁移到 cpp/,只保留必要的 Python 接口。

测试与文档:保持测试与文档的良好组织,使得项目易于维护和扩展。

CMake配置:在顶层 CMakeLists.txt 中配置整个项目的构建逻辑,并通过 add_subdirectory 包含 cpp 目录中的 CMake 配置。Cython部分的构建可以通过 setup.py 进行配置。

setup.py配置:在 setup.py 中管理 Python 和 Cython 的安装和打包。如果需要,将 C++/Pybind11 的构建与 Python 包打包集成,可以在 setup.py 中调用 CMake 进行构建。后面再具体总结setup.py的配置。

VSCode与CMake

image-20240829211205568

文件夹

  • 3DPhononMC:这是你当前打开的 CMake 项目的根目录名称。VS Code 识别这是一个 CMake 项目,并在这个文件夹下组织所有与 CMake 相关的内容。

配置

  • Visual Studio 生成工具 2022 Release - x64 - Debug:这个部分显示了当前选择的 CMake 配置,包括编译器和构建类型。在这个例子中,使用的是 Visual Studio 2022 发行版生成工具,当前是 Debug 构建类型。
  • 你可以通过点击这个部分来选择其他配置(不同的编译器或不同的构建类型)。

生成

  • test_hello:这里列出了你 CMake 项目中可构建的目标。test_hello 是一个可执行目标,表示这个项目会生成一个名为 test_hello 的可执行文件。
  • 点击生成目标可以触发构建。你也可以编辑目标配置(如重命名或修改构建选项)。

测试

  • [All tests]:这部分显示项目中定义的测试目标。如果你使用了 GoogleTest 或其他测试框架,并在 CMake 中定义了测试,CMake Tools 会在这里列出这些测试,并允许你运行它们。
  • [All tests] 表示你可以一次运行项目中所有的测试。

调试

  • all:这一部分列出了可以调试的构建目标。在这个例子中,all 可能是一个自动生成的目标,表示可以调试整个项目。
  • 点击调试目标后,VS Code 会启动调试器,并附加到选定的可执行文件或测试上。

启动

  • all:这一部分通常与调试目标类似,但它只是启动目标而不附加调试器。
  • 你可以点击这里来启动项目的主可执行文件或其他指定的启动目标。

项目大纲

  • PHONONMC:这是你的 CMake 项目在项目大纲中的显示名称。VS Code 通过 CMakeLists.txt 文件识别并组织项目结构。
  • hello_cpp (静态库)hello_py (静态库):这些是项目中的库目标(静态库)。这些目标是由 CMakeLists.txt 文件定义的,并且会生成 .lib.a 文件。
  • test_hello (可执行):这是一个可执行文件目标,表示项目将生成一个名为 test_hello 的可执行文件。

总结

  • 文件夹 部分显示项目的根目录。
  • 配置 部分允许你选择和切换构建配置(如编译器和生成器)。
  • 生成 部分显示可以生成的目标文件(可执行文件、静态库等)。
  • 测试 部分显示项目中定义的单元测试,可以直接运行这些测试。
  • 调试启动 部分列出可以调试或启动的目标。
  • 项目大纲 显示项目的整体结构和构建目标。

这些部分相互配合,使你能够方便地配置、构建、测试和调试 CMake 项目。

可以在CMake插件的默认选项中设置默认生成器,比如Ninja。此外,在VSCODE工具栏左侧的测试部分,可以看到项目的详细测试目标。

在包含子模块的CMake测试中,每个层级的CMakeLists.txt都需要enable_testing(),否则在生成目录的顶层运行ctest是找不到子目录的测试目标的。