自动驾驶C++实时中间件:PuppetMaster 重构记录,阶段一:项目骨架整理

13 阅读4分钟

最近我开始整理一个 C++ 实时中间件项目:PuppetMaster

它不是想一上来就做一个庞大的通用框架,而是从我之前维护过的自动驾驶中间件中,逐步提炼出一套更清晰、更通用、更容易复用的基础能力:模块通信、任务调度、Topic 管理、通信后端抽象、错误处理、生命周期管理,以及后续的配置和可观测性。

GitHub 地址:

github.com/SoleyRan/Pu…

这一篇记录的是第一阶段:项目骨架整理

这一阶段要解决什么?

我创建的Issue标题是:

Refine project skeleton

这个 issue 定的目标很直接:

  • Refine 架构
  • 稳定项目骨架
  • 让项目成为一个规范的 C++ 库
  • 使用 C++17
  • 后续能继续演进为一个可复用的中间件项目

在这一步之前,项目里已经有一些旧代码和迁移中的模块,但是这些代码还没有被整理成一个稳定的开源项目结构。

所以第一步的重点不是把所有旧代码都编进来,而是先建立一个稳定、清楚、可构建、可安装、可引用的工程骨架。

整理后的项目结构

第一阶段完成之后,项目结构变成了这样:

.
|-- CMakeLists.txt
|-- README.md
|-- LICENSE
|-- cmake/
|   |-- PuppetMasterConfig.cmake.in
|   `-- puppet_master/
|       `-- version.h.in
|-- include/
|   `-- puppet_master/
|       |-- puppet_master.h
|       |-- export.h
|       `-- core/
|           |-- status.h
|           `-- types.h
|-- src/
|   |-- CMakeLists.txt
|   |-- README.md
|   `-- puppet_master.cpp
|-- demo/
|   |-- CMakeLists.txt
|   `-- skeleton_demo.cpp
|-- test/
|   |-- CMakeLists.txt
|   `-- project_skeleton_test.cpp
`-- docs/
    `-- architecture.md

这个结构里有几个比较重要的变化:

  • include/puppet_master 作为公共头文件目录
  • src 只放库实现和内部迁移区
  • demo 放最小示例
  • test 放 smoke test
  • cmake 放 package config 和版本模板
  • docs 放架构说明
  • 主库 target 命名为 PuppetMaster::PuppetMaster

这样后续别人使用这个项目时,不需要关心内部目录怎么组织,只需要链接一个稳定的 CMake target

CMake 顶层整理

第一阶段我把顶层 CMakeLists.txt 整理成更适合开源库的形式:

cmake_minimum_required(VERSION 3.16)

project(PuppetMaster
    VERSION 0.1.0
    DESCRIPTION "A C++ real-time middleware runtime for message-driven components and deterministic task scheduling."
    HOMEPAGE_URL "https://github.com/SoleyRan/PuppetMaster"
    LANGUAGES CXX
)

include(GNUInstallDirs)
include(CMakePackageConfigHelpers)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)

这里做了几件事:

  • 明确最低 CMake 版本
  • 明确项目版本
  • 明确项目描述
  • 关闭编译器扩展,使用标准 C++17
  • 引入 GNUInstallDirs
  • 引入 CMake package config辅助工具

然后增加几个项目选项:

option(PUPPETMASTER_BUILD_DEMOS "Build PuppetMaster example programs." ${PUPPETMASTER_IS_TOP_LEVEL})
option(PUPPETMASTER_BUILD_TESTS "Build PuppetMaster tests." ${PUPPETMASTER_IS_TOP_LEVEL})
option(PUPPETMASTER_ENABLE_FASTDDS "Enable FastDDS integration hooks. The adapter build lands in a later branch." OFF)

这里我用了 PUPPETMASTER_ 前缀,避免以后这个项目被别人用 add_subdirectory() 引入时,选项名污染外部工程。

同时 demo 和 test 默认只在顶层构建时打开。如果 PuppetMaster 被作为子项目引入,就不会默认把 demo/test 也带进去。

主库 target

真正的主库 target 放在 src/CMakeLists.txt 里:

add_library(puppet_master
    puppet_master.cpp
    ${PUPPETMASTER_PUBLIC_HEADERS}
)

add_library(PuppetMaster::PuppetMaster ALIAS puppet_master)

对外推荐使用的是:

PuppetMaster::PuppetMaster

也就是说,后续用户项目里可以这样引用:

find_package(PuppetMaster CONFIG REQUIRED)
target_link_libraries(my_component PRIVATE PuppetMaster::PuppetMaster)

我比较喜欢这种方式,因为它把“库文件叫什么”和“用户应该怎么链接”分开了。

内部 target 可以叫 puppet_master,但外部用户只需要记住命名空间 target:

PuppetMaster::PuppetMaster

include 路径

主库的 include 路径这样配置:

target_include_directories(puppet_master
    PUBLIC
        "$<BUILD_INTERFACE:${PROJECT_SOURCE_DIR}/include>"
        "$<BUILD_INTERFACE:${PUPPETMASTER_GENERATED_INCLUDE_DIR}>"
        "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>"
)

这里区分了两种情况:

  • build tree 里使用源码目录和生成目录
  • install 后使用安装目录

生成目录主要用来放版本头文件:

puppet_master/version.h

对应模板是:

#pragma once

namespace puppet_master {

inline constexpr int kVersionMajor = @PROJECT_VERSION_MAJOR@;
inline constexpr int kVersionMinor = @PROJECT_VERSION_MINOR@;
inline constexpr int kVersionPatch = @PROJECT_VERSION_PATCH@;
inline constexpr const char* kVersion = "@PROJECT_VERSION@";

}  // namespace puppet_master

这样代码里可以直接访问版本信息:

puppet_master::kVersion

符号导出

为了后续支持动态库,我加了一个 export.h

#pragma once

#if defined(PUPPET_MASTER_STATIC_DEFINE)
#    define PUPPET_MASTER_API
#elif defined(_WIN32) || defined(__CYGWIN__)
#    if defined(PUPPET_MASTER_EXPORTS)
#        define PUPPET_MASTER_API __declspec(dllexport)
#    else
#        define PUPPET_MASTER_API __declspec(dllimport)
#    endif
#else
#    if defined(PUPPET_MASTER_EXPORTS)
#        define PUPPET_MASTER_API __attribute__((visibility("default")))
#    else
#        define PUPPET_MASTER_API
#    endif
#endif

现在项目还很小,但这个文件越早加越好。如果等接口很多之后再补导出宏,就会变成一次很烦的批量修改。

最小实现

第一阶段的实现文件也很简单:

#include <puppet_master/puppet_master.h>

namespace puppet_master {

const char* ProjectName() noexcept
{
    return "PuppetMaster";
}

const char* Version() noexcept
{
    return kVersion;
}

}  // namespace puppet_master

这一阶段我没有急着把旧的 FastDDS、logger、queue、timer 全部编进主库。

原因很简单:项目骨架整理应该保持边界清楚

如果第一步就把通信层、日志层、调度层一起接进来,这个分支会变得非常大,也不利于后续 review 和回滚。

demo

为了验证主库 target 能被正常链接,我加了一个最小 demo:

#include <iostream>

#include <puppet_master/puppet_master.h>

int main()
{
    std::cout << puppet_master::ProjectName() << " "
              << puppet_master::Version() << '\n';
    return 0;
}

对应 CMake:

add_executable(puppet_master_skeleton_demo
    skeleton_demo.cpp
)

target_link_libraries(puppet_master_skeleton_demo
    PRIVATE
        PuppetMaster::PuppetMaster
)

这个 demo 没有复杂逻辑,只做一件事:证明外部 executable 可以通过 PuppetMaster::PuppetMaster 引用主库。

smoke test

测试里也加了一个最小 smoke test:

#include <cassert>
#include <cstring>

#include <puppet_master/puppet_master.h>

int main()
{
    assert(std::strcmp(puppet_master::ProjectName(), "PuppetMaster") == 0);
    assert(puppet_master::Version()[0] != '\0');
    assert(puppet_master::kVersionMajor == 0);

    const puppet_master::core::Status ok;
    assert(ok.ok());

    const auto invalid =
        puppet_master::core::Status::InvalidArgument("missing topic name");
    assert(!invalid.ok());
    assert(invalid.code() == puppet_master::core::StatusCode::kInvalidArgument);
    assert(!invalid.message().empty());

    return 0;
}

这个测试也不复杂,主要验证:

  • public header 能正常 include
  • 项目名称能正常访问
  • 版本信息能正常生成
  • 最基础的 Status 类型可用

对应 CMake:

add_executable(puppet_master_project_skeleton_test
    project_skeleton_test.cpp
)

target_link_libraries(puppet_master_project_skeleton_test
    PRIVATE
        PuppetMaster::PuppetMaster
)

add_test(
    NAME puppet_master.project_skeleton
    COMMAND puppet_master_project_skeleton_test
)

测试截图:

屏幕截图 2026-05-05 214548.png

install/export 支持

作为一个准备长期整理的中间件项目,我希望它后续可以被其他工程正常 find_package()

所以第一阶段也加了 package config:

configure_package_config_file(
    "${PROJECT_SOURCE_DIR}/cmake/PuppetMasterConfig.cmake.in"
    "${PROJECT_BINARY_DIR}/PuppetMasterConfig.cmake"
    INSTALL_DESTINATION "${PUPPETMASTER_CMAKE_INSTALL_DIR}"
)

write_basic_package_version_file(
    "${PROJECT_BINARY_DIR}/PuppetMasterConfigVersion.cmake"
    VERSION "${PROJECT_VERSION}"
    COMPATIBILITY SameMajorVersion
)

target 导出:

install(
    EXPORT PuppetMasterTargets
    NAMESPACE PuppetMaster::
    DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/PuppetMaster"
)

安装头文件:

install(
    DIRECTORY "${PROJECT_SOURCE_DIR}/include/"
    DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}"
)

这样后续安装之后,外部工程就可以写:

find_package(PuppetMaster CONFIG REQUIRED)
target_link_libraries(app PRIVATE PuppetMaster::PuppetMaster)

README 和架构文档

第一阶段也重写了 README。

README 里主要说明:

  • PuppetMaster 是什么
  • 当前状态
  • 项目目标
  • 目录结构
  • 构建方式
  • 下游如何使用
  • 后续路线

同时新增了:

docs/architecture.md

里面记录了后续准备拆分的几层:

Core
Transport
Runtime
Component
Scheduler
Tooling
flowchart TB
    App["业务工程 / Demo / Test"] --> Target["PuppetMaster::PuppetMaster"]

    Target --> PublicAPI["Public Headers<br/>include/puppet_master"]
    Target --> Impl["Internal Implementation<br/>src/"]
    Target --> Package["CMake Install / Export<br/>find_package(PuppetMaster)"]

    PublicAPI --> Entry["puppet_master.h<br/>统一入口"]
    PublicAPI --> Export["export.h<br/>符号导出"]
    PublicAPI --> Version["version.h<br/>版本信息"]

    Impl --> SrcCMake["src/CMakeLists.txt"]
    Impl --> MinimalImpl["puppet_master.cpp<br/>最小实现"]

    App --> Demo["demo/skeleton_demo.cpp"]
    App --> Tests["test/project_skeleton_test.cpp"]

    Package --> Config["PuppetMasterConfig.cmake"]
    Package --> Install["install/export target"]

    Docs["docs/architecture.md"] -.说明设计边界.-> PublicAPI
    Docs -.记录迁移策略.-> Impl

快速运行

整理之后,预期的构建方式是:

cmake -S . -B build
cmake --build build
ctest --test-dir build

如果不想构建 demo:

cmake -S . -B build -DPUPPETMASTER_BUILD_DEMOS=OFF

如果不想构建 test:

cmake -S . -B build -DPUPPETMASTER_BUILD_TESTS=OFF

总结

第一阶段没有做很复杂的功能。

它主要解决的是一个基础问题:让 PuppetMaster 先成为一个规范的 C++ 项目,而不是一堆代码文件的集合。

这一阶段完成之后,项目有了:

  • 稳定的公共 include 入口
  • 清晰的 CMake target
  • demo 和 test 入口
  • version header 生成
  • install/export 基础
  • 文档目录
  • 后续架构分层说明

这些东西看起来不像通信算法或者调度器那样“有功能感”,但对一个中间件项目来说非常重要。 因为后面的所有能力,都会长在这个骨架上。

下一篇我会继续整理第二阶段:Core Types 与错误模型。也就是把 StatusResult<T>TopicNameTaskNameMessagePolicy 这些基础概念先提炼出来,为后面的通信抽象和运行时调度做准备。

项目地址:

github.com/SoleyRan/Pu…

Issue:

github.com/SoleyRan/Pu…

PR:

github.com/SoleyRan/Pu…