跳转至

C++模块化

前言

C++20标准引入了模块功能,它可以加速项目构建,避免宏传播污染,是更加现代的编译方式。 而 Vulkan-hpp 也提供了内置的模块封装,我们将在本章介绍如何使用它。

理论上可以加快,但由于C++社区模块化进度缓慢,标准库仍在实验中,导致大量头文件需放于全局模块片段。 而模块编译有顺序要求,多因素综合反而减缓了编译速度。
但无论如何,模块化是未来的方向,且使用它编写的代码更加“优雅”可观。

本章将分为三部分,第一部分介绍 vulkam.cppm 模块文件的用法,第二部分给出一个模块化的示例项目,第三部分介绍其他工具的模块化(待定)。

建议使用 CLion / Visual Studio 等编辑器,它们可以自动识别 CMake 预设且对 C++ 模块有良好的支持( VSCode 智能感知对模块支持不佳)。

Vulkan模块

1. 基础代码

在开始内容阅读之前,请下载下面这份超级简单的基础代码:

点击下载

请仔细浏览基础代码,目前的内容非常简单,仅输出 Hello, World!

也可以选择手动编译运行,请查看 CMakePresets.json 文件并选择合适的预设,以 MSVC 为例:

cmake --preset "win-x64"
cmake --build --preset "win-x64-build-debug"
./build/Debug/main.exe

你将看到输出 Hello, World!

2. 辅助CMake文件

Vulkan-hpp提供了模块文件,并在官方仓库的 README 中给出了使用说明。

目前还不支持预构建的模块,我们需要使用 vulkan 文件夹下的 vulkan.cppm 手动编译。 我们的基础代码有个 cmake 文件夹是空的,现在在里面创建一个 VulkanHppModule.cmake 文件。

项目根目录/
│
├── CMakeLists.txt     # 主CMake配置文件
├── CMakePresets.json  # CMake预设文件
├── cmake/             # CMake脚本目录
│   │
│   └── VulkanHppModule.cmake # Vulkan模块脚本
└── src/          # 源代码目录
    │
    └── main.cpp  # 主程序入口

3. 编译脚本

由于 vulkan-hpp 已经提供了 vulkan.cppm 文件,实际的模块编译非常简单,直接使用它即可。

首先查找库并判断版本,我假设你的 Vulkan 版本是 1.4 以上:

# VulkanHppModule.cmake
# IF 判断,防止重复编译
if(NOT TARGET VulkanHppModule)
    find_package(Vulkan REQUIRED)
    message(Vulkan SDK version: ${Vulkan_VERSION})
    if(${Vulkan_VERSION} VERSION_LESS "1.4.0") # 版本判断,可选
        message(FATAL_ERROR "Vulkan SDK too old (required ≥ 1.4.0)")
    endif()
endif()

成功找到库后,我们可以通过 Vulkan_INCLUDE_DIR 找到模块文件并编译它:

if(NOT TARGET VulkanHppModule)
    find_package(Vulkan REQUIRED)
    # 版本判断,此处忽略,可自行添加
    add_library( VulkanHppModule )
    target_sources(VulkanHppModule PUBLIC
            FILE_SET CXX_MODULES
            BASE_DIRS ${Vulkan_INCLUDE_DIR}
            FILES ${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm
    )
    target_link_libraries( VulkanHppModule PRIVATE Vulkan::Vulkan )
endif()

我们添加了一个库目标 VulkanHppModule ,后面可以在主 CMake 配置中链接它。 编译模块需要用到 CXX_MODULES 文件集,这里直接找到了 SDK 提供的模块文件并使用它进行编译。

如果你查看过 vulkan.cppm 的内容,你会发现它只是导入了 Vulkan 头文件并在 export 块内用 using 将标识符导出,所以我们要为它提供头文件和源文件,即 Vulkan::Vulkan

有些必要的宏定义在头文件中,但是我们链接 Vulkan::Vulkan 时使用 PRIVATE 保证了封闭性,使用此库的人无法访问头文件。 一种解决方案是在为它链接一个 PUBLIC 的仅头文件库:

target_link_libraries( VulkanHppModule PRIVATE Vulkan::Vulkan )
target_link_libraries( VulkanHppModule PUBLIC Vulkan::Headers )

现在还剩下最后一件事,就是编译配置。我们在“实例”章节提到过 Vulkan-hpp 需要使用“动态加载器”自动加载函数指针,此外还有一些可选的编译配置比如“禁用异常”可以按需配置:

# VulkanHppModule.cmake
if(NOT TARGET VulkanHppModule)

    find_package(Vulkan REQUIRED)
    # 版本判断,此处忽略,可自行添加
    add_library( VulkanHppModule )
    target_sources(VulkanHppModule PUBLIC
            FILE_SET CXX_MODULES
            BASE_DIRS ${Vulkan_INCLUDE_DIR}
            FILES ${Vulkan_INCLUDE_DIR}/vulkan/vulkan.cppm
    )
    target_link_libraries( VulkanHppModule PRIVATE Vulkan::Vulkan )
    target_link_libraries( VulkanHppModule PUBLIC Vulkan::Headers )

    target_compile_definitions(VulkanHppModule PUBLIC
            VULKAN_HPP_DISPATCH_LOADER_DYNAMIC=1 # 启用动态加载器
            # 其他可选配置参考 vulkan-hpp 仓库的 README
    )
endif()

最后还要在 CMakeLists.txt 中使用它:

# CMakeLists.txt
cmake_minimum_required(VERSION 4.0.0)

project(HelloCppModule LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)

include(cmake/VulkanHppModule.cmake)

add_executable(main src/main.cpp)

target_link_libraries(main PRIVATE VulkanHppModule)

模块的编译需要 C++20 标准,我们已在 CMakeLists.txt 中使用 set 设置。

4. 编译测试

现在可以回到 main.cpp 文件添加一些测试代码:

#include <iostream>
#include <vulkan/vulkan_hpp_macros.hpp> # 此头文件包含hpp封装常用的部分宏
import vulkan_hpp;

int main() {
    vk::raii::Context ctx; // 初始化上下文

    vk::ApplicationInfo app_info = {
        "My App", 1,
        "My Engine", 1,
        vk::makeApiVersion(1, 0, 0, 0)
    };
    vk::InstanceCreateInfo create_info{ {}, &app_info };
    vk::raii::Instance instance = ctx.createInstance(create_info);

    auto physicalDevices = instance.enumeratePhysicalDevices();
    for (const auto& physicalDevice : physicalDevices) {
        std::cout << physicalDevice.getProperties().deviceName << std::endl;
    }
}

我们尝试初始化上下文、创建实例和获得可用的物理设备信息。

现在你可以尝试编译项目并运行,使用编辑器集成或者前面介绍的手动编译都可以。

编译失败可以尝试删除 build 文件夹后重新编译,或者更新 Vulkan 版本。

正常运行应该看到类似下面的信息:

NVIDIA GeForce RTX ...
Intel(R) UHD Graphics ...
...... 或其他显卡设备

Vulkan 的模块化就到这里,后面会介绍本课程其他可能用到的库的模块化方式。

项目模块化示例

请下载下面的示例代码,此代码将“移动摄像机”的代码进行了模块化拆分,使得结构更加清晰:

点击下载

我们后面的章节可能将此代码作为基础代码进行扩展,请务必下载并查看,它的最终效果与之前“移动摄像机”章节的最终效果完全相同:

right_room

即使已经过去多年,26标准即将到来,C++20的模块编译依然不是一件容易成功的事。 本实例代码在作者的几台设备上都可以正常编译运行,但在你的设备上可能由于 Vulkan、CMake 或其他依赖库的版本问题导致编译失败,或许需要根据自己的环境进行一些调整。

即使无法运行,你也可以直接查看 src/ 文件夹内的代码。 这很有助于你了解 Vulkan 各组件的依赖关系,因为它们现在严格分离在多个模块文件中,而不像之前一样挤在一起。

其他工具的模块化

1. GLM模块

// TODO

2. GLFW模块

// TODO

3. stb_image模块

// TODO

4. tinyobjloader模块

// TODO

5. VkMemoryAllocator模块

// TODO

6. imgui模块

// TODO


第一部分:

main.cpp样例

CMakeLists.txt样例

CMakePresets.json样例

VulkanHppModule.cmake样例

第二部分:

module_code.zip

请下载后本地查看(UTF-8),浏览器查看文件可能出现中文乱码。


评论