Compile-time, type-safe publish-subscribe message bus in C++17 template metaprogramming (header-only). Bilingual EN/中文 docs + from-scratch tutorial.
  • C++ 89.2%
  • CMake 10.8%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
Secaumene 2037fea929 Ignore *.log and clean up stray crashpad debug.log
The VS Code CLI (Electron/crashpad) drops a debug.log into the working
directory; ignore *.log so such noise never enters the repo.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-07-11 15:04:40 +09:00
.vscode Add portable VS Code debug config (launch.json) 2026-07-11 02:34:04 +09:00
docs Add per-topic subscriber capacity and a topics code generator 2026-07-11 01:43:52 +09:00
examples Add per-topic subscriber capacity and a topics code generator 2026-07-11 01:43:52 +09:00
include/sms Add per-topic subscriber capacity and a topics code generator 2026-07-11 01:43:52 +09:00
tests Add per-topic subscriber capacity and a topics code generator 2026-07-11 01:43:52 +09:00
tools Add per-topic subscriber capacity and a topics code generator 2026-07-11 01:43:52 +09:00
.clang-format Reorganize into a documented, tested, portable pub-sub library 2026-07-11 00:43:28 +09:00
.gitignore Ignore *.log and clean up stray crashpad debug.log 2026-07-11 15:04:40 +09:00
CLAUDE.md Enable VS Code C++ IntelliSense (portable, no hardcoded paths) 2026-07-11 01:58:23 +09:00
CMakeLists.txt Default to a Debug build for source-level debugging 2026-07-11 03:00:56 +09:00
README.md Add per-topic subscriber capacity and a topics code generator 2026-07-11 01:43:52 +09:00

StaticMessageServer · 静态消息服务器

A compile-time, type-safe publish–subscribe message bus written in modern C++17 template metaprogramming. 一个用现代 C++17 模板元编程实现的、编译期类型安全的发布–订阅消息总线。

English · 中文


English

What it is / What it is NOT

This project is an in-process message bus: parts of your program publish messages to a topic, and any number of subscribers registered on that topic receive them. It is not a network server — there are no sockets, ports, or protocols. Everything happens inside one process, resolved at compile time for full type safety and zero runtime type-checking.

About the name. The original folder was StasticMessageSever, a double typo of Static Message Server (静态消息服务器). "Static" refers to the static per-topic storage and the compile-time (static) dispatch; it has nothing to do with statistics. The code has been corrected to Server, subscriber, etc.

Features

  • Compile-time type-safe messages — a message is a set of named fields ("tags"); reading a field returns its exact declared type.
  • Type erasure done right — values live behind std::shared_ptr<void> with a custom deleter, so any type can be stored and safely recovered.
  • CRTP subscribers — write a subscriber by inheriting one base class and providing two small hooks.
  • Per-topic broker with configurable capacity — init / subscribe / unsubscribe / publish / count; the max-subscriber count is a template parameter declared with the topic (CRDict<MaxSubscribers, Tags...>).
  • Multiple heterogeneous subscribers and multiple independent topics.
  • Statistics aggregation example (count / sum / average / min / max).
  • Code generator (topicc) — describe your topics in a tiny .topics DSL and generate the compile-time topic header automatically (wired into the CMake build).
  • Portable CMake build — no hardcoded compiler path, works with g++, clang++, or MSVC on any OS.
  • Header-only — just add include/ to your include path.

Project layout

include/sms/   crdict.hpp · subscriber.hpp · server.hpp · static_message_server.hpp (umbrella)
tools/topicc/  topicc.cpp — the ".topics" → C++ header code generator
examples/      01_basic_pubsub · 02_logging_subscriber · 03_multi_topic · 04_statistics_subscriber
               05_generated_topics · weather.topics (DSL sample)
tests/         test_main.hpp (tiny framework) · test_crdict · test_server · test_statistics
docs/          DEVELOPMENT.md  ← the from-scratch tutorial
CMakeLists.txt · README.md · CLAUDE.md · .clang-format

Requirements

  • A C++17 compiler: g++, clang++, or MSVC — whichever is on your PATH.
  • CMake ≥ 3.16.

Nothing else. No third-party libraries.

Build & run

cmake -S . -B build          # discovers your compiler from PATH (prints e.g. "GNU 16.1.0")
cmake --build build          # builds all examples + tests, warning-clean

# run an example
build/examples/01_basic_pubsub          # -> a=2 b=1

# run the test suite
ctest --test-dir build --output-on-failure   # -> 100% tests passed

On Windows the executables end in .exe (e.g. build\examples\01_basic_pubsub.exe). With Visual Studio's multi-config generator, add --config Debug to the build/ctest commands.

Quick start

#include <iostream>
#include <sms/static_message_server.hpp>

struct Price { using type = double; };            // 1. declare a tag (a field)
struct Topic : sms::CRDict<8, Price> {};          // 2. declare a topic (8 = max subscribers)

template <typename T>                             // 3. write a subscriber (CRTP)
struct Watcher : sms::SubscriberBase<T, Watcher<T>> {
    template <typename In> void CallFunction(const In& m) {
        std::cout << "price = " << m.template Get<Price>() << "\n";
    }
    void CallFunctionFromVoidPtr(const void* p) {
        CallFunction(*static_cast<const decltype(T::Create())*>(p));
    }
};

int main() {
    sms::Server<Topic>::init();
    sms::Server<Topic>::subscribe(new Watcher<Topic>());     // 4. subscribe
    sms::Server<Topic>::publish(Topic::Create().Set<Price>(9.99));  // 5. publish
}

API cheatsheet

Type Member Meaning
sms::CRDict<MaxSubscribers, Tags...> static Create() make an empty message; leading N = subscriber capacity
Value (from Create()) .Set<Tag>(v) set a field (chainable, rvalue-only)
.Get<Tag>() read a field (throws if unset)
.Has<Tag>() is the field set?
sms::SubscriberBase<Topic, Self> CallFunction(const In&) you write: your logic
CallFunctionFromVoidPtr(const void*) you write: recover type, call above
sms::Server<Topic> init() clear the table
subscribe(ptr / shared_ptr) register (→ bool)
unsubscribe(ptr / shared_ptr) remove (→ bool)
publish(message) deliver to all
count() number of subscribers
kMaxSubscribers capacity, from the topic's CRDict<N, …>

Examples

File Shows
01_basic_pubsub one topic, one subscriber, one message
02_logging_subscriber two different subscriber types on one topic
03_multi_topic two independent topics with separate tables
04_statistics_subscriber aggregating a stream into count/sum/avg/min/max
05_generated_topics topics generated from a .topics DSL by topicc

Code generation with topicc

Instead of hand-writing tag structs and CRDict topics, you can describe them in a small .topics DSL and let the topicc generator emit the header:

# examples/weather.topics
topic Weather 16          # 16 = max subscribers
    temperature : double
    humidity    : double
    pressure    : int
# build/tools/topicc/topicc  (built as part of the project)
topicc examples/weather.topics weather_topics.hpp

produces:

namespace topics {
struct Weather_temperature { using type = double; };
struct Weather_humidity { using type = double; };
struct Weather_pressure { using type = int; };
struct Weather : sms::CRDict<16, Weather_temperature, Weather_humidity, Weather_pressure> {};
}

The CMake build runs topicc automatically to feed examples/05_generated_topics.cpp (see examples/CMakeLists.txt). Details in docs/DEVELOPMENT.md Ch8.

Learn how it works

Read docs/DEVELOPMENT.md — a chapter-by-chapter tutorial that rebuilds this entire project from an empty folder, aimed at C++ beginners.

Future / advanced (not implemented)

  • Thread safety. The bus is single-threaded; concurrent publish/subscribe needs locks (see DEVELOPMENT.md Appendix B).
  • Dynamic capacity (grow beyond kMaxSubscribers), richer statistics, or serialization for cross-process delivery.

中文

它是什么,不是什么

本项目是一个进程内消息总线:程序的某些部分把消息发布(publish)到某个主题(topic),注册在该主题上的任意多个*订阅者(subscriber)*都会收到。它不是网络服务器——没有套接字、端口或协议。一切都在同一个进程内发生,并在编译期完成解析,从而获得完整的类型安全、零运行期类型检查。

关于名字。 原文件夹名为 StasticMessageSever,是 Static(静态)Message(消息)Server(服务器)的双重笔误。“Static/静态”指每个主题的 static 静态存储与编译期(静态)分发,与“统计(statistics)”无关。代码已更正为 Server、subscriber 等规范拼写。

特性

  • 编译期类型安全的消息——一条消息是一组具名字段(“标签/tag”);读取字段返回它声明的确切类型。
  • 正确的类型擦除——值以带自定义删除器的 std::shared_ptr<void> 存放,任何类型都能被存入并安全取回。
  • CRTP 订阅者——继承一个基类并提供两个小钩子函数即可写出订阅者。
  • 每主题一个代理、容量可配置——init / subscribe / unsubscribe / publish / count;最大订阅者数量是随主题一起声明的模板参数(CRDict<最大订阅者数, Tags...>)。
  • 多个异构订阅者与多个相互独立的主题。
  • 统计聚合示例(计数 / 求和 / 平均 / 最小 / 最大)。
  • 代码生成器(topicc)——用一个小巧的 .topics 描述语言描述主题,自动生成编译期主题头文件(已接入 CMake 构建)。
  • 可移植的 CMake 构建——不硬编码编译器路径,g++、clang++、MSVC 及任意操作系统均可。
  • 纯头文件——把 include/ 加入包含路径即可。

目录结构

include/sms/   crdict.hpp · subscriber.hpp · server.hpp · static_message_server.hpp(伞头文件)
tools/topicc/  topicc.cpp —— 把 ".topics" 描述转成 C++ 头文件的代码生成器
examples/      01_basic_pubsub · 02_logging_subscriber · 03_multi_topic · 04_statistics_subscriber
               05_generated_topics · weather.topics(DSL 示例)
tests/         test_main.hpp(小框架)· test_crdict · test_server · test_statistics
docs/          DEVELOPMENT.md  ← 从零重建教程
CMakeLists.txt · README.md · CLAUDE.md · .clang-format

环境要求

  • 一个 C++17 编译器:g++、clang++ 或 MSVC——你 PATH 上有哪个都行。
  • CMake ≥ 3.16。

除此之外别无所需,不依赖任何第三方库。

构建与运行

cmake -S . -B build          # 从 PATH 发现你的编译器(会打印如 "GNU 16.1.0")
cmake --build build          # 构建所有示例与测试,无告警

# 运行某个示例
build/examples/01_basic_pubsub          # -> a=2 b=1

# 运行测试套件
ctest --test-dir build --output-on-failure   # -> 100% tests passed

Windows 上可执行文件带 .exe 后缀(如 build\examples\01_basic_pubsub.exe)。若用 Visual Studio 的多配置生成器,请给构建/ctest 命令加上 --config Debug。

快速上手

见上方英文 Quick start 代码;五步:①声明标签 ②声明主题 ③写订阅者(CRTP)④订阅 ⑤发布。

API 速查表

见上方英文 API cheatsheet。要点:

  • CRDict<最大订阅者数, Tags...>::Create() 造空消息;.Set<Tag>(v) 链式设值(只能对右值调用);.Get<Tag>() 取值(未设则抛异常);.Has<Tag>() 判断是否已设。
  • 订阅者继承 SubscriberBase<Topic, Self>,实现 CallFunction(你的逻辑)与 CallFunctionFromVoidPtr(还原类型再调用前者)。
  • Server<Topic>:init / subscribe / unsubscribe / publish / count,容量 kMaxSubscribers 在主题声明处(CRDict<N, …>)给出。

示例

文件 演示
01_basic_pubsub 单主题、单订阅者、单消息
02_logging_subscriber 同一主题上两种不同订阅者
03_multi_topic 两个相互独立、各自独立表的主题
04_statistics_subscriber 把消息流聚合为 计数/求和/平均/最小/最大
05_generated_topics 使用由 topicc 从 .topics 描述语言生成的主题

用 topicc 生成代码

不必手写标签结构体和 CRDict 主题,你可以用一个小巧的 .topics 描述语言来描述它们,再让 topicc 生成器产出头文件:

# examples/weather.topics
topic Weather 16          # 16 = 最大订阅者数
    temperature : double
    humidity    : double
    pressure    : int
topicc examples/weather.topics weather_topics.hpp

会生成 struct Weather : sms::CRDict<16, Weather_temperature, Weather_humidity, Weather_pressure> {}; 等声明(标签名形如 <主题>_<字段>)。CMake 构建会自动运行 topicc 来喂给 examples/05_generated_topics.cpp。详见 docs/DEVELOPMENT.md 第 8 章。

想弄懂原理?

请读 docs/DEVELOPMENT.md——一份从空文件夹开始、逐章重建整个项目的教程,面向 C++ 新手。

未来 / 进阶(尚未实现)

  • 线程安全。当前总线是单线程的;并发 publish/subscribe 需要加锁(见 DEVELOPMENT.md 附录 B)。
  • 动态容量(突破 kMaxSubscribers)、更丰富的统计、或用于跨进程投递的序列化。