C++项目结构及交叉编译

2025-01-16 22:04:53

# 前言

C++的项目结构一直是个难点。虽然有着 vcpkg 等工具,但当前的C++项目仍主要采用 cmake 进行管理,由于这种构建脚本的方式,以及现代化语言的包管理器(类似 cargo、pip/uv 或 go mod)的缺失,因此也没有一个统一的项目结构。另外,C++的交叉编译也无法做到开箱即用,需要另外配置。

本篇主要介绍一下我个人在项目中所认为的合理结构,以及在 WSL 下开发 Windows 应用程序的过程。

# 干净的 C++项目结构

下面是一个简单的 opengl 项目的项目结构。

.
├── build
│   ├── CMakeFiles
│   ├── third_party
│   ├── CMakeCache.txt
│   ├── Makefile
│   ├── cmake_install.cmake
│   ├── compile_commands.json
│   └── opengl-example.exe
├── include
│   ├── app.h
│   └── err.h
├── src
│   ├── app.cpp
│   ├── err.cpp
│   └── main.cpp
├── third_party
│   ├── glad
│   └── glfw-3.4
├── .clang-format
├── .gitignore
├── CMakeLists.txt
├── justfile
└── toolchain.cmake

对于一些较复杂的项目,需要拆分成多个库,可在根目录新建以该库为名的新的子目录,且每个库的结构应当与上面的结构一致。

# 一个干净的 CMakeLists 文件

一个干净的 CMakeLists 文件应当有良好的分类以及简洁的注释。

# project
set(name "opengl-example")
cmake_minimum_required(VERSION 3.10)
project(${name} CXX)

# clang setting
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)

# src
include_directories(include)
file(GLOB SRC src/*.cpp)
add_executable(${name} ${SRC})

# link std library static
target_link_options(${name} PRIVATE -static-libgcc -static-libstdc++)

# third party
add_subdirectory(./third_party/glfw-3.4)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "" FORCE)
target_link_libraries(${name} glfw)

find_package(OpenGL REQUIRED)
target_link_libraries(${name} OpenGL::GL)

add_subdirectory(./third_party/glad)
target_link_libraries(${name} glad)

# 交叉编译

如果要进行交叉编译,首先需要准备工具链以及相对应的工具链文件。对于 Linux 编译 Windows 程序,一般使用 mingw。

对于 ArchLinux 来说,需要安装 mingw-w64-gcc,这会安装该程序和与之对应的头文件。

sudo pacman -S mingw-w64-gcc

在项目根目录创建 toolchain.cmake 文件,并填入以下内容。

set(CMAKE_SYSTEM_NAME Windows)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(TOOLCHAIN_PREFIX x86_64-w64-mingw32)

# cross compilers to use for C, C++ and Fortran
set(CMAKE_C_COMPILER ${TOOLCHAIN_PREFIX}-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_PREFIX}-g++)
set(CMAKE_Fortran_COMPILER ${TOOLCHAIN_PREFIX}-gfortran)
set(CMAKE_RC_COMPILER ${TOOLCHAIN_PREFIX}-windres)

# target environment on the build host system
set(CMAKE_FIND_ROOT_PATH /usr/${TOOLCHAIN_PREFIX})

# modify default behavior of FIND_XXX() commands
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)

然后指定该工具链,生成构建文件:

cmake -B build -DCMAKE_TOOLCHAIN_FILE=./toolchain.cmake

之后可正常开始编译:

cmake --build build

# 通用的构建脚本写法

对于c++项目来说,有些命令会多次执行,因此一般都会配置一个构建脚本。一般使用较多的包括初始化、编译、运行、清理。这里提供一个通用的构建脚本写法(基于 justfile):

init:
  cmake -B build

init_windows:
  cmake -B build -DCMAKE_TOOLCHAIN_FILE=./toolchain.cmake

build:
  cmake --build build

run:
  #!/usr/bin/bash
  just build
  name=$(cat build/CMakeCache.txt | grep CMAKE_PROJECT_NAME | awk -F '=' '{print $2}') 
  ./build/$name.exe

clean:
  rm -rf build

其中 init 开头的指令用以生成构建系统文件,仅在初始化时执行。

# 其他

一般来说,代码格式化风格全凭个人喜好,但是其中一项设置可能导致项目出错,因此建议始终保持如下设置(在 .clang-format 中):

SortIncludes: Never