最近我开始整理一个 C++ 实时中间件项目:PuppetMaster
它不是想一上来就做一个庞大的通用框架,而是从我之前维护过的自动驾驶中间件中,逐步提炼出一套更清晰、更通用、更容易复用的基础能力:模块通信、任务调度、Topic 管理、通信后端抽象、错误处理、生命周期管理,以及后续的配置和可观测性。
GitHub 地址:
这一篇记录的是第一阶段:项目骨架整理
这一阶段要解决什么?
我创建的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 testcmake放 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
)
测试截图:
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 与错误模型。也就是把 Status、Result<T>、TopicName、TaskName、MessagePolicy 这些基础概念先提炼出来,为后面的通信抽象和运行时调度做准备。
项目地址:
Issue:
PR: