- C++ 89.2%
- CMake 10.8%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
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]> |
||
| .vscode | ||
| docs | ||
| examples | ||
| include/sms | ||
| tests | ||
| tools | ||
| .clang-format | ||
| .gitignore | ||
| CLAUDE.md | ||
| CMakeLists.txt | ||
| README.md | ||
StaticMessageServer · 静态消息服务器
A compile-time, type-safe publish–subscribe message bus written in modern C++17 template metaprogramming. 一个用现代 C++17 模板元编程实现的、编译期类型安全的发布–订阅消息总线。
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 thestaticper-topic storage and the compile-time (static) dispatch; it has nothing to do with statistics. The code has been corrected toServer,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.topicsDSL 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)、更丰富的统计、或用于跨进程投递的序列化。