SpacetimeDB C++ 快速上手:5 分钟用 C++ 编写可编译为 WebAssembly 的服务端模块
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
本篇指南基于 SpacetimeDB 官方 C++ 快速入门文档展开,完整覆盖从安装 Emscripten 工具链、创建basic-cpp模板项目,到编写表(Table)与 Reducer、并用 CLI 实时调用与查询的完整流程。读完本文,你将掌握使用 C++20 编写 SpacetimeDB 服务端模块的核心套路,并理解spacetime dev背后由 CMake +emcc驱动编译为 WebAssembly 的真实工作链路,可直接上手搭建自己的 C++ 后端模块。
前置条件
在开始之前,请确认本机已具备以下环境:
| 依赖 | 版本要求 | 用途 |
|---|---|---|
| SpacetimeDB CLI | 最新稳定版 | 创建项目、启动本地服务、构建发布模块、生成客户端绑定 |
| Emscripten SDK | 4.0.21 及以上 | 提供emcc编译器与 Emscripten CMake 工具链文件 |
| CMake | 3.20 及以上 | 驱动 C++ 模块的配置与构建 |
| make / ninja | 任一后端 | CMake 的构建后端 |
| C++20 工具链(宿主) | C++20 | 语法编译,最终通过 Emscripten 交叉编译为 WASM 目标 |
安装完 Emscripten SDK 后,必须执行对应的emsdk_env脚本(PowerShell 或 Bash),使emcc以及 CMake 工具链文件进入PATH,否则后续的spacetime build无法找到编译器。
第一步:安装并激活 Emscripten
SpacetimeDB 的 C++ 模块最终会被编译为 WebAssembly(WASM)运行在服务端,因此需要 Emscripten 提供的emcc编译器。推荐使用 4.0.21 及以上版本,按如下方式安装并激活环境:
# 从你的 emsdk 目录执行(下载/clone 之后) # Windows PowerShell ./emsdk install 4.0.21 ./emsdk activate 4.0.21 ./emsdk_env.ps1 # macOS/Linux ./emsdk install 4.0.21 ./emsdk activate 4.0.21 source ./emsdk_env.sh激活后建议先验证工具链是否可用:
emcc --version cmake --version从 CLI 的源码实现看,spacetime build在构建 C++ 模块前会依次检查emcc、cmake、emcmake是否存在于PATH(见 crates/cli/src/tasks/cpp.rs)。其中 Windows 平台会查找emcc.bat、cmake.exe,macOS/Linux 则查找无后缀的可执行文件;任一工具缺失都会直接报错中止构建。因此这一步的"激活环境"并非可选项,而是构建流程的硬性前置。
第二步:创建你的 C++ 项目
SpacetimeDB CLI 提供了模板驱动的开发流程。spacetime dev会为你完成一整条链路:启动本地服务器、构建并发布模块(内部封装了 CMake +emcc)、生成客户端绑定代码:
spacetime dev --template basic-cpp该命令的核心是--template basic-cpp,对应的模板位于仓库的 templates/basic-cpp 目录。如果你更喜欢手动控制构建过程,也可以直接驱动 CMake +emcc(可参考模板内的 CMakeLists.txt),但官方推荐路径始终是spacetime build/spacetime dev,因为前者会替你处理好工具链检查、WASM 链接参数、输出产物定位等一系列细节。
CLI 是如何构建 C++ 模块的
spacetime dev最终会调用 CLI 内部的build_cpp逻辑(见 crates/cli/src/tasks/mod.rs 中ModuleLanguage::Cpp => build_cpp(...)的分支)。从 crates/cli/src/tasks/cpp.rs 可以还原出它的实际执行步骤:
- 校验
emcc/cmake/emcmake三个可执行文件; - 执行
emcmake cmake -S . -B build -DCMAKE_BUILD_TYPE=<Debug|Release>完成配置(spacetime dev走 Debug,发布构建走 Release); - 执行
cmake --build build --config <build_type> --parallel进行编译链接; - 递归扫描
build/目录,取修改时间最新的.wasm文件作为模块产物(避免误用旧缓存)。
理解了这一链路,你就知道为什么"必须先激活 Emscripten 环境":emcmake是 Emscripten 提供的 CMake 包装器,缺少它整个配置阶段都无法启动。
第三步:探索项目结构
spacetime dev --template basic-cpp生成的项目结构如下:
my-spacetime-app/ ├── spacetimedb/ # 你的 C++ 模块 │ ├── CMakeLists.txt │ └── src/ │ └── lib.cpp # 服务端逻辑 ├── Cargo.toml └── src/ ├── module_bindings/ # 自动生成的 Rust 客户端绑定 └── main.rs # Rust 客户端应用其中关键文件与真实仓库模板的对应关系为:
- 服务端模块:lib.cpp 是模块的入口,表结构与所有 Reducer 都写在这里;CMakeLists.txt 负责把
lib.cpp编译为 WASM 并链接 C++ 绑定 SDK。 - 客户端绑定:
src/module_bindings/由 CLI 根据模块 schema 自动生成(见 templates/basic-cpp/src/module_bindings/mod.rs 顶部的 "AUTOMATICALLY GENERATED BY SPACETIMEDB" 注释,内含person_table.rs、person_type.rs、add_reducer.rs、say_hello_reducer.rs等文件)。直接修改这些文件是无效的,改动表结构应回到lib.cpp重新构建生成。 - Rust 客户端:main.rs 演示了连接数据库、订阅
person表、注册on_insert回调的完整客户端写法,依赖的spacetimedb-sdk版本在 Cargo.toml 中声明。
CMakeLists.txt 关键配置速览
模板的 CMakeLists.txt 值得细读,它揭示了 C++ 模块的编译细节:
SPACETIMEDB_CPP_VERSION:SDK 版本选择器,MAJOR.MINOR(如2.10)会拉取release/MAJOR.MINOR分支的最新补丁,MAJOR.MINOR.PATCH则锁定vMAJOR.MINOR.PATCH精确标签;SPACETIMEDB_CPP_REF:直接覆盖 Git ref(如release/latest、main、v1.12.0);SPACETIMEDB_CPP_DIR:指向本地 C++ 绑定的 clone 目录,用于绑定开发场景(会跳过 FetchContent 网络拉取);- 标准设为 C++20(
CMAKE_CXX_STANDARD 20); - 在 Emscripten 平台下追加
-fno-exceptions、-O2 -g0编译选项,并链接-sSTANDALONE_WASM=1、--no-entry、-sINITIAL_MEMORY=16MB等 WASM 专用参数,导出_malloc、_free、___describe_module__、___call_reducer__等运行时入口函数。
第四步:理解表(Table)与 Reducer
模板自带的lib.cpp展示了 SpacetimeDB C++ 模块最核心的两个抽象——表(服务端持久化数据)与Reducer(客户端可远程调用的服务端函数)。模板包含一张Person表和两个 Reducer:add用于插入数据,say_hello用于遍历并打印日志:
#include "spacetimedb.h" using namespace SpacetimeDB; struct Person { std::string name; }; SPACETIMEDB_STRUCT(Person, name) SPACETIMEDB_TABLE(Person, person, Public) SPACETIMEDB_REDUCER(add, ReducerContext ctx, std::string name) { ctx.db[person].insert(Person{name}); return Ok(); } SPACETIMEDB_REDUCER(say_hello, ReducerContext ctx) { for (const auto& person : ctx.db[person]) { LOG_INFO("Hello, " + person.name + "!"); } LOG_INFO("Hello, World!"); return Ok(); }三个宏的职责划分
这段代码中的三个宏构成了 C++ 模块的基本骨架(完整参考见 skills/cpp-server/SKILL.md):
SPACETIMEDB_STRUCT(Person, name):把 C++ struct 注册为 SpacetimeDB 的行类型(product type);SPACETIMEDB_TABLE(Person, person, Public):把该类型注册为一张表,第二个参数person是访问器名(accessor),后续通过ctx.db[person]访问;第三个参数Public/Private控制表是否对客户端可见;还可以传入第四个参数true将其声明为事件表(event table);SPACETIMEDB_REDUCER(add, ReducerContext ctx, std::string name):注册一个可被客户端调用的 Reducer,第一个参数是 reducer 名称(即调用时使用的名字),第二个参数是上下文ctx,其余是 reducer 入参。
Reducer 的上下文与返回值约定
- 所有 Reducer 都以
ReducerContext ctx开头,通过ctx访问ctx.db[表访问器](表访问)、ctx.sender()(调用者 Identity)、ctx.timestamp(确定性时间戳)、ctx.rng()(确定性随机数)等; - 每个 Reducer 必须返回
ReducerResult:成功用Ok(),失败用Err("错误信息"),Err会把错误信息返回给调用方; - 表字段约束通过字段宏声明,例如
FIELD_PrimaryKey(accessor, field)(主键)、FIELD_PrimaryKeyAutoInc(accessor, field)(自增主键,插入时传 0)、FIELD_Unique(accessor, field)(唯一约束)、FIELD_Index(accessor, field)(B 树索引,启用.filter()查询)。
生命周期钩子
模板的lib.cpp中还包含三个常用的生命周期钩子(真实模板源码见 templates/basic-cpp/spacetimedb/src/lib.cpp):
SPACETIMEDB_INIT(init, ReducerContext ctx):模块首次发布时调用,适合做初始化逻辑;SPACETIMEDB_CLIENT_CONNECTED(identity_connected, ReducerContext ctx):每当新客户端连接时调用;SPACETIMEDB_CLIENT_DISCONNECTED(identity_disconnected, ReducerContext ctx):每当客户端断开时调用。
第五步:用 CLI 测试你的模块
spacetime dev会在前台运行并持续监听。打开另一个终端,进入项目目录,即可通过 CLI 直接调用 Reducer、执行 SQL 查询并查看日志:
cd my-spacetime-app # 插入一个人(调用 add reducer) spacetime call add Alice # 查询 person 表 spacetime sql "SELECT * FROM person" # 调用 say_hello,向所有人打招呼 spacetime call say_hello # 查看模块日志 spacetime logs预期行为:
spacetime call add Alice会在person表插入一行{ name: "Alice" };spacetime sql "SELECT * FROM person"返回当前表内全部数据;spacetime call say_hello遍历表内每一行并打出Hello, <name>!日志,随后输出一行Hello, World!;spacetime logs展示模块的标准输出日志,包括上面所有LOG_INFO的内容。
与此同时,如果你保持main.rs的 Rust 客户端在运行,它订阅了person表并注册了on_insert回调,插入新行时终端还会打印New person: Alice,这直观展示了"服务端表变更实时推送至客户端"的订阅机制(见 templates/basic-cpp/src/main.rs)。
注意事项与常见问题
使用本地 SDK 克隆
默认情况下,CMake 通过 FetchContent 从远端拉取 C++ 绑定 SDK。如果你要使用本仓库内的本地绑定(仓库内即包含 crates/bindings-cpp 的完整 C++ 绑定源码),可以在运行spacetime dev/spacetime build之前设置环境变量SPACETIMEDB_CPP_SDK_DIR指向本地目录:
# Bash / macOS / Linux export SPACETIMEDB_CPP_SDK_DIR=/path/to/SpacetimeDB/crates/bindings-cpp # Windows PowerShell $env:SPACETIMEDB_CPP_SDK_DIR="E:\SpacetimeDB\crates\bindings-cpp"该变量最终会落入 CMake 的SPACETIMEDB_CPP_DIR逻辑(见 templates/basic-cpp/spacetimedb/CMakeLists.txt),从而跳过 FetchContent 网络拉取,直接以add_subdirectory方式链接本地绑定,适合 SDK 二次开发场景。
异常处理被禁用
模板构建出的 WASM 模块默认禁用异常(编译选项-fno-exceptions,同时链接-sDISABLE_EXCEPTION_CATCHING=1)。这意味着模块代码中不应依赖 C++ 异常机制来传递错误,错误处理应遵循 SpacetimeDB 的约定——在 Reducer 中返回Err(...),并通过ReducerResult传播给调用方。
emcc 找不到怎么办
如果执行spacetime dev/spacetime build时提示emcc未找到,说明 Emscripten 环境未激活或PATH未持久化。重新运行对应的emsdk_env脚本(Windows 用emsdk_env.ps1,macOS/Linux 用source ./emsdk_env.sh)以填充环境变量后重试。CLI 在 Windows 上会额外通过cmd /C包装命令来兼容emcc.bat/emcmake.cmd这类批处理可执行文件,这一点在 crates/cli/src/tasks/cpp.rs 有明确实现。
延伸:从模板走向真实项目
模板只是起点。如果要在真实项目中使用 C++ 编写 SpacetimeDB 模块,可以结合仓库内 skills/cpp-server/SKILL.md 这份完整的 SDK 参考继续深入,它覆盖了以下常用能力:
- 更丰富的字段约束:
FIELD_PrimaryKey/FIELD_PrimaryKeyAutoInc/FIELD_Unique/FIELD_Index/FIELD_NamedMultiColumnIndex(多列索引),以及基于索引的filter("Alice")、范围查询range_inclusive(18, 65)等; - CRUD 操作:
ctx.db[table].insert(...)、.find(key)(返回std::optional)、.filter(...)、.count()、查找后修改再update(*e)、delete_by_key(...); - 自定义类型:
SPACETIMEDB_UNIT_TYPE/SPACETIMEDB_ENUM定义枚举(sum type); - 定时表(Scheduled Tables):通过
SPACETIMEDB_SCHEDULE宏声明定时表,用ScheduleAt::time(...)做一次性触发、ScheduleAt::interval(...)做周期触发; - 鉴权与时间:
ctx.sender()校验调用者身份、ctx.timestamp获取确定性服务端时间戳、ctx.sender_auth()读取 JWT claims; - 日志分级:
LOG_INFO/LOG_WARN/LOG_ERROR/LOG_DEBUG/LOG_PANIC。
仓库内还有完整的多语言客户端示例与测试(如 crates/bindings-cpp/tests),以及基于 C++ 的完整示例模块 modules/benchmarks-cpp、demo/Blackholio/server-cpp 等,可以作为阅读源码、理解真实用法的参考。
【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考