SpacetimeDB C++ 模块库实战:用 C++20 在数据库内构建 WebAssembly 模块
2026/9/12 0:01:43 网站建设 项目流程

SpacetimeDB C++ 模块库实战:用 C++20 在数据库内构建 WebAssembly 模块

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

导读

SpacetimeDB C++ 模块库(crates/bindings-cpp)为 C++ 开发者提供了一套现代 C++20 API,用于编写编译为 WebAssembly 并在 SpacetimeDB 数据库内部运行的模块,从而将应用服务端逻辑直接下沉到数据库中,省去独立的业务服务层。本文围绕该库的官方 README 展开,完整覆盖其功能清单、架构设计、环境准备、快速上手、构建与发布流程、全套宏 API 参考,并结合仓库内的源码与示例模块(ARCHITECTURE.md、REFERENCE.md、modules/module-test-cpp/src/lib.cpp、modules/sdk-test-cpp/src/lib.cpp)做纵深讲解。读完本文,你将能够独立创建一个 C++ SpacetimeDB 模块:定义带约束的表、编写 reducer / view / procedure、构建出.wasm并发布到数据库,同时理解其底层类型注册与校验机制。

一、库定位与功能概览

SpacetimeDB C++ 模块库的核心定位是:用 C++20 编写运行在数据库内部的 WebAssembly 模块。与传统“数据库 → 应用服务器 → 客户端”的三层架构不同,模块直接在数据库内执行业务逻辑,客户端通过订阅实时获得数据同步。

该库提供了“生产就绪”的 C++ 绑定,覆盖了完整的类型系统支持,README 中列出的功能包括:

  • 模块编译与发布:源码经 Emscripten 编译为 WASM 后发布到 SpacetimeDB;
  • 全部生命周期 reducerinitclient_connectedclient_disconnected
  • 用户自定义 reducer:支持不限数量的参数;
  • 表注册与约束PrimaryKeyUniqueAutoInc
  • 插入 / 更新 / 删除操作:基于类型安全的表访问器;
  • 全部基础类型u8~u256i8~i256boolf32f64string
  • 全部特殊类型IdentityConnectionIdTimestampTimeDurationUuidResult<>
  • 向量类型:所有基础类型与特殊类型均可构成std::vector<T>
  • 可选类型std::optional<T>
  • 自定义结构体的 BSATN 序列化
  • 复杂枚举:支持带载荷的变体枚举及正确的变体命名;
  • 增强日志系统:带文件 / 行号信息的多级别日志。

进阶能力

除基础功能外,README 还列出了可直接使用的高级特性:

特性支撑宏 / API说明
Btree 索引FIELD_Index配合range_from()range_to()range_inclusive()等实现优化查询
范围查询range_queries.h完整的区间查询体系
客户端可见性过滤SPACETIMEDB_CLIENT_VISIBILITY_FILTER行级安全,基于 SQL 谓词控制客户端可见行
定时 reducerSPACETIMEDB_SCHEDULE基于ScheduleAt字段的时间驱动执行
过程(procedure)SPACETIMEDB_PROCEDURE返回值的纯函数,可用显式事务访问数据库
视图(view)SPACETIMEDB_VIEW只读查询函数,返回std::vector<T>std::optional<T>
字段访问器模式ctx.db[table_field]基于索引的高效操作

这些能力在仓库中均有可运行的示例:见modules/*-cpp/src/lib.cpp,其中 modules/module-test-cpp/src/lib.cpp 覆盖了索引、范围查询、枚举、调度、HTTP 处理器等用法,modules/sdk-test-cpp/src/lib.cpp 则是一个与 Rust / C# 测试模块完全等价的全量类型与操作测试模块(2091 行,覆盖每种基础类型、向量、可选、Result、唯一约束、主键表与调度表)。

二、架构设计:混合编译期 / 运行期体系

README 用四个要点概括了库的架构(详细技术文档见 crates/bindings-cpp/ARCHITECTURE.md):

  1. 混合编译期 / 运行期系统(Hybrid Compile-Time/Runtime System):C++20 concepts 在编译期完成校验,__preinit__函数在 WASM 模块加载时执行运行期注册;
  2. V9 类型注册系统:统一类型注册,具备全面的错误检测与循环引用防护;
  3. 名义类型系统(Nominal Type System):类型通过其声明的名字而非结构分析来标识,通过SPACETIMEDB_STRUCT等宏显式注册;
  4. 多层校验(Multi-Layer Validation):静态断言 → 运行期约束检查 → 错误模块替换策略,覆盖从编译到发布的全链路。

2.1 优先级有序的初始化系统

核心机制是带编号的__preinit__导出函数(实现于 crates/bindings-cpp/src/internal/Module.cpp),保证初始化顺序确定:

__preinit__01_ - 清理全局状态(最先执行) __preinit__10_ - 字段注册 __preinit__19_ - 自增集成与定时 reducer __preinit__20_ - 表与生命周期 reducer 注册 __preinit__21_ - 字段约束 __preinit__25_ - 行级安全过滤 __preinit__30_ - 用户 reducer __preinit__40_ - 视图 __preinit__50_ - 过程 __preinit__99_ - 类型校验与错误检测(最后执行)

为什么需要编号?因为注册存在严格依赖:表必须先于约束存在,类型必须先于引用被注册,而校验必须发生在所有注册完成之后。WASM 线性内存模型要求确定性初始化,因此这种顺序是硬约束而非约定。

2.2 类型注册与循环引用防护

类型注册协调器是 V9Builder,但所有类型处理都委托给统一的ModuleTypeRegistration系统(crates/bindings-cpp/include/spacetimedb/internal/module_type_registration.h)。其核心原则是:只有用户自定义的结构体和枚举才进入类型空间(typespace),基础类型、数组、Optional 和特殊类型始终内联。注册流程依次检查:基础类型 → 数组 → Option → 特殊类型 → 用户自定义类型(注册并返回引用)。循环引用通过types_being_registered_集合跟踪,发现重复注册会立即设置全局错误标志并返回错误类型。

2.3 错误模块替换策略

__preinit__99_validate_types中依次检查三类错误:循环引用(ERROR_CIRCULAR_REFERENCE_<type>)、多个主键(ERROR_MULTIPLE_PRIMARY_KEYS_<table>)、类型注册错误(ERROR_TYPE_REGISTRATION_<message>)。一旦命中,正常模块会被替换为包含无效类型引用的“错误模块”,SpacetimeDB 解析该类型时即失败,并把描述性错误名返回给开发者——这是从编译期到服务端的最后一道防线。

三、环境准备

构建 C++ 模块需要以下工具链:

依赖版本要求用途
SpacetimeDB CLI最新版初始化、构建、发布、调用 reducer、执行 SQL
Emscripten SDK (emsdk)最新版将 C++ 编译为 WebAssembly
CMake3.16+(库的CMakeLists.txt声明最低 3.15)构建系统
C++ 编译器支持 C++20编译源码

库本身的 CMake 配置见 crates/bindings-cpp/CMakeLists.txt:静态库目标spacetimedb_cpp_library(别名spacetimedb::spacetimedb_cpp_library),强制cxx_std_20;在 Emscripten 环境下会自动附加-O2 -fno-exceptions -ffunction-sections -fdata-sections -Wall -Wextra编译选项,其中-fno-exceptions是 WASM 兼容性的关键——这也解释了为什么错误处理采用返回值而非异常。

四、快速上手

方式一:spacetime init(推荐)

# 创建一个新的 C++ 项目 spacetime init --lang cpp my-project cd my-project # 构建并发布 spacetime build -p ./spacetimedb spacetime publish -p ./spacetimedb my-database

spacetime init --lang cpp生成的项目结构为:

my-chat-module/spacetimedb/ ├── CMakeLists.txt ├── src/ └── lib.cpp └── .gitignore

方式二:手动搭建

对已有项目,在 C++ 模块中加入以下代码即可。这是 README 给出的完整最小示例,涵盖了表、枚举、约束、reducer、生命周期 reducer、视图与过程:

#include <spacetimedb.h> using namespace SpacetimeDB; // 定义表结构 struct User { Identity identity; std::string name; std::string email; }; // 注册 BSATN 序列化 SPACETIMEDB_STRUCT(User, identity, name, email) // 注册为公共表 SPACETIMEDB_TABLE(User, users, Public) // 使用 FIELD_ 宏添加约束 FIELD_PrimaryKey(users, identity); FIELD_Unique(users, email); // 定义带命名空间限定的枚举 SPACETIMEDB_ENUM(UserRole, Admin, Moderator, Member) SPACETIMEDB_NAMESPACE(UserRole, "Auth") // 客户端代码中显示为 "Auth.UserRole" // 用户自定义 reducer SPACETIMEDB_REDUCER(add_user, ReducerContext ctx, std::string name, std::string email) { User user{ctx.sender(), name, email}; // id 将自动生成 ctx.db[users].insert(user); LOG_INFO("Added user: " + name); return Ok(); } // 按主键删除用户 SPACETIMEDB_REDUCER(delete_user, ReducerContext ctx) { ctx.db[users_identity].delete_by_key(ctx.sender()); return Ok(); } // 生命周期 reducer(可选) SPACETIMEDB_INIT(init, ReducerContext ctx) { LOG_INFO("Module initialized"); return Ok(); } SPACETIMEDB_CLIENT_CONNECTED(on_connect, ReducerContext ctx) { LOG_INFO("Client connected: " + ctx.sender().to_hex_string()); return Ok(); } SPACETIMEDB_CLIENT_DISCONNECTED(on_disconnect, ReducerContext ctx) { LOG_INFO("Client disconnected: " + ctx.sender().to_hex_string()); return Ok(); } // 定义视图:查询调用者自身的用户记录 SPACETIMEDB_VIEW(std::optional<User>, find_my_user, Public, ViewContext ctx) { // 使用索引字段按 identity 查找 return ctx.db[users_identity].find(ctx.sender()); } // 定义过程(带返回值的纯函数) SPACETIMEDB_PROCEDURE(uint32_t, add_numbers, ProcedureContext ctx, uint32_t a, uint32_t b) { return a + b; }

4.1 手动搭建时的 CMake 配置

若不用spacetime init,参考 crates/bindings-cpp/REFERENCE.md 中的 CMake 配置(库的CMakeLists.txt也支持MODULE_SOURCEOUTPUT_NAME两个缓存变量,默认分别为src/lib.cpplib):

cmake_minimum_required(VERSION 3.16) project(my-module) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 指向仓库中的 C++ 绑定目录 set(SPACETIMEDB_CPP_LIBRARY_PATH "path/to/crates/bindings-cpp") add_executable(lib src/lib.cpp) target_include_directories(lib PRIVATE ${SPACETIMEDB_CPP_LIBRARY_PATH}/include) add_subdirectory(${SPACETIMEDB_CPP_LIBRARY_PATH} spacetimedb_cpp_library) target_link_libraries(lib PRIVATE spacetimedb_cpp_library) # Emscripten 下的 WASM 设置 if(CMAKE_SYSTEM_NAME STREQUAL "Emscripten") set_target_properties(lib PROPERTIES SUFFIX ".wasm" LINK_FLAGS "-s STANDALONE_WASM=1 ..." ) endif()

头文件入口为单头文件 crates/bindings-cpp/include/spacetimedb.h,其中按模块系统、表与约束、reducer、procedure、视图等分组聚合了全部子头文件。

五、构建与发布模块

构建步骤

# 进入模块目录 cd modules/your-module # 构建项目 spacetime build -p ./spacetimedb # 发布到 SpacetimeDB(显式指定 wasm 路径) spacetime publish --bin-path ./spacetimedb/build/lib.wasm your-database-name # 或直接给目录(自动检测 build/lib.wasm) spacetime publish ./spacetimedb your-database-name

自定义模块源码

若要构建不同的源文件,可覆盖 CMake 变量(仓库的编译测试即采用此方式,参考 crates/bindings-cpp/tests/compile/run-compile-tests.sh):

# 构建指定的测试模块 emcmake cmake -B build -DMODULE_SOURCE=src/test_module.cpp -DOUTPUT_NAME=test_module . cmake --build build # 生成 build/test_module.wasm

完整的手动构建 / 发布命令

# 使用 spacetime spacetime build -p . # 手动构建 emcmake cmake -B build . cmake --build build # 发布 spacetime publish . my-database # 或手动指定产物 spacetime publish --bin-path build/lib.wasm my-database

启动本地实例后可先用 CLI 验证模块行为(来自 crates/bindings-cpp/QUICKSTART.md):

spacetime start # 启动本地 SpacetimeDB spacetime call my-db set_name "Alice" # 调用 reducer spacetime sql my-db "SELECT * FROM user" # 查询数据

六、宏 API 参考

表定义

说明
SPACETIMEDB_TABLE(Type, table_name, Public/Private)注册一张表
SPACETIMEDB_STRUCT(Type, field1, field2, ...)为类型注册 BSATN 序列化

可见性规则:Public 表自动同步给订阅客户端;Private 表仅 reducer 可访问、不同步给客户端。同一个结构体可注册为多张表(例如一张私有错误日志表加一张公共审计日志表)。

枚举定义

说明
SPACETIMEDB_ENUM(EnumName, Value1, Value2, ...)定义简单枚举(单元变体)
SPACETIMEDB_ENUM(EnumName, (Variant1, Type1), (Variant2, Type2), ...)定义带载荷的变体枚举
SPACETIMEDB_NAMESPACE(EnumName, "Namespace")为枚举添加命名空间限定

变体枚举底层是std::variant,因此每个载荷类型必须唯一——需要多个单元变体时,用SPACETIMEDB_UNIT_TYPE(Name)创建唯一的空类型(参见 modules/module-test-cpp/src/lib.cpp 中TestFFoo/TestFBar的做法,这是与 C# SDK 直接复用Unit的差异点)。

Reducer

说明
SPACETIMEDB_REDUCER(name, ReducerContext ctx, ...)用户自定义 reducer
SPACETIMEDB_INIT(name, ReducerContext ctx)模块初始化 reducer(可选)
SPACETIMEDB_CLIENT_CONNECTED(name, ReducerContext ctx)客户端连接 reducer(可选)
SPACETIMEDB_CLIENT_DISCONNECTED(name, ReducerContext ctx)客户端断开 reducer(可选)

Reducer 的关键语义:

  • 返回ReducerResult(即Outcome<void>的类型别名);
  • 成功用return Ok();,失败用return Err("message");
  • 返回Err会触发整个事务回滚,错误消息序列化后返回调用方,不会导致 WASM 崩溃;
  • 第一个参数必须是ReducerContext ctx,其余参数由客户端传入且必须已注册序列化。

ReducerContext提供的能力包括:ctx.sender()(调用方身份)、ctx.timestamp(当前时间戳)、ctx.rng()(确定性随机数,以 reducer 时间戳微秒为种子)、ctx.database_identity()ctx.sender_auth()(JWT 鉴权)以及ctx.db[...]数据库访问。

视图(View)

说明
SPACETIMEDB_VIEW(return_type, name, Public/Private, ViewContext ctx)只读查询函数
SPACETIMEDB_VIEW(return_type, name, Public/Private, AnonymousViewContext ctx)匿名视图(无发送方身份)

注意:视图当前只支持 context 参数,尚不支持额外的调用参数。

过程(Procedure)

说明
SPACETIMEDB_PROCEDURE(return_type, name, ProcedureContext ctx, ...)返回值的纯函数

要点:

  • 直接返回类型本身(不包裹在Outcome中),可为任意 SpacetimeType(基础类型、结构体、枚举、Unit等);
  • 访问数据库需要显式事务:ctx.WithTx()ctx.TryWithTx()
  • 始终公开,无访问控制。

字段约束(在表注册后应用)

说明
FIELD_PrimaryKey(table_name, field)主键约束
FIELD_PrimaryKeyAutoInc(table_name, field)自增主键
FIELD_Unique(table_name, field)唯一约束
FIELD_UniqueAutoInc(table_name, field)自增唯一字段
FIELD_Index(table_name, field)索引(btree),加速查询与范围操作
FIELD_IndexAutoInc(table_name, field)自增索引字段
FIELD_AutoInc(table_name, field)仅自增,无其他约束
FIELD_NamedMultiColumnIndex(table, index_name, col1, col2)多列 btree 索引
FIELD_Default(table, field, value)字段默认值(迁移 / 加列场景)

约束的合法类型有硬性要求(见 crates/bindings-cpp/REFERENCE.md):

约束类型允许的类型
PrimaryKey整数、bool、string、Identity、ConnectionId、Timestamp、枚举
Unique同 PrimaryKey
Index同 PrimaryKey
AutoInc仅整数类型

这些限制在编译期由 C++20 concepts(如FilterableValueAutoIncrementable,定义于 crates/bindings-cpp/include/spacetimedb/table_with_constraints.h)和static_assert强制执行,违反会得到带字段名和指引的清晰编译错误。

自增回调机制

使用自增字段时,insert()会自动返回带生成 ID 的行对象。底层流程(ARCHITECTURE.md 有完整描述):insert()序列化并发送行 → 服务端生成自增值 → 服务端仅回传生成的列值(BSATN 格式)→ SDK 调用注册在__preinit__19_的集成函数把生成值写回原行 →insert()返回填充完整的行。因此插入后立即可用生成 ID:

User user{0, "Bob", true}; // id=0 是占位符,将被自动生成 User inserted = ctx.db[user].insert(user); LOG_INFO("Created user with ID: " + std::to_string(inserted.id));

支持多个自增字段共存(例如FIELD_PrimaryKeyAutoIncFIELD_UniqueAutoInc同时存在时,所有生成值都会被集成)。

七、日志系统

LOG_DEBUG("Debug message"); LOG_INFO("Info message"); LOG_WARN("Warning message"); LOG_ERROR("Error message"); LOG_PANIC("Fatal error message"); // 带计时 { LogStopwatch timer("Operation name"); // ... 需要计时的代码 ... } // 离开作用域时自动输出耗时

实现位于 crates/bindings-cpp/include/spacetimedb/logger.h,日志包含文件 / 行号等源码位置信息。日志级别还可通过模块源码顶部的#define STDB_LOG_LEVEL覆盖(modules/module-test-cpp/src/lib.cpp 中即设置为TRACE)。

八、数据库访问模式详解

C++ 绑定使用独特的双访问器模式(QUICKSTART.md 称之为 “unique accessor pattern”):

  • ctx.db[tableName]—— 表访问,用于迭代和基础操作,如ctx.db[user].insert(...)for (const auto& row : ctx.db[user])ctx.db[user].count()
  • ctx.db[tableName_fieldName]—— 字段访问器,用于基于索引的高效操作,如ctx.db[user_identity].find(...)delete_by_key(...)filter(...)
操作表访问字段访问(索引)
Insertinsert(row)
Delete手动迭代delete_by_key(key)
Updateupdate(row)update(row)
查询迭代filter(value)

范围查询

对已建FIELD_Index的字段可进行范围查询(头文件 crates/bindings-cpp/include/spacetimedb/range_queries.h):

auto range1 = range_from(25); // 25.. (>= 25) auto range2 = range_to(30); // ..30 (< 30) auto range3 = range(20, 35); // 20..35(>= 20, < 35) auto range4 = range_inclusive(20, 35); // 20..=35(>= 20, <= 35) auto range5 = range_to_inclusive(30); // ..=30 auto range6 = range_full<int>(); // 全量 bool in_range = range4.contains(25); // true // 对索引字段过滤,字符串区间同样可用 for (const auto& product : ctx.db[product_item_price].filter(price_range)) { LOG_INFO("Product in range: " + product.name); }

modules/module-test-cpp/src/lib.cpp 的test_btree_index_argsreducer 对整数、多列坐标、字符串三类区间做了完整验证,并对比了“基于索引的范围过滤”与“全表手动过滤”的结果一致性。

九、类型系统细节

基础类型与容器

C++ 绑定支持全部标准整数 / 浮点类型,外加 SpacetimeDB 专属的大整数类型u128u256i128i256,以及std::stringstd::vector<T>std::optional<T>Result<T, E>。注意类型必须精确匹配(例如用uint32_t而非int,QUICKSTART.md 的故障排查一节特别强调了这一点)。

命名空间限定系统

SPACETIMEDB_NAMESPACE(EnumName, "Prefix")是一个纯编译期特性:它通过模板特化SpacetimeDB::detail::namespace_info<T>存储常量字符串,LazyTypeRegistrar在注册时用if constexpr检测命名空间并拼接限定名(如Auth.UserRole)。客户端代码生成器据此在 TypeScript、C#、Rust 客户端中组织类型,而服务端 C++ 代码仍使用未限定的名字。优点是零运行期开销、可选且向后兼容(详见 crates/bindings-cpp/ARCHITECTURE.md 的命名空间章节)。

十、已知限制

README 明确列出了当前版本的边界:

  1. 类型系统

    • 非常大的类型组合可能超过 WASM 内存限制;
    • 复杂递归类型引用需要仔细安排注册顺序。
  2. 数据库操作

    • 基于索引的操作使用字段访问器:ctx.db[table_field].delete_by_key(value)
    • 表约束由服务端声明并强制实施;
    • 通过字段访问器支持 insert / delete / update。
  3. 高级特性

    • FIELD_Index创建 btree 索引以支持高效范围查询;
    • 支持range_from()range_to()range_inclusive()等完整范围查询体系;
    • 行级安全通过SPACETIMEDB_CLIENT_VISIBILITY_FILTER实现;
    • 迁移能力有限:仅支持自动添加表;
    • SQL 执行:仅能通过 CLI(spacetime sql)使用,模块内部不可执行 SQL。

此外,ARCHITECTURE.md 也提示其内容中仍保留少量历史实现描述,当前实现已精简为生产就绪状态。

十一、示例模块与测试

README 指向modules/*-cpp/src/目录下的示例:

  • modules/module-test-cpp/src/lib.cpp —— 与 Rustmodule-test等价:约束 / 索引 / 枚举 / 视图 / 调度 reducer / JWT 鉴权 / 过程 / HTTP 处理器全覆盖,并包含大量范围查询验证;
  • modules/sdk-test-cpp/src/lib.cpp —— 全量类型与操作测试模块,与 Rust、C# SDK 测试模块完全等价:每种基础类型与特殊类型的单值表、向量表、可选表、Result 表、唯一约束表、主键表,以及对应的 insert / delete / update reducer。

测试体系方面,仓库还提供了:

  • 类型隔离测试:crates/bindings-cpp/tests/type-isolation-test/(含error_circular_ref.cpperror_multiple_pk.cpperror_autoinc_non_integer.cpp等负向用例与module01~module12的正向用例);
  • 编译期校验测试:crates/bindings-cpp/tests/compile/cases/;
  • 查询构建器编译与 SQL 测试:crates/bindings-cpp/tests/query-builder-compile/、crates/bindings-cpp/tests/query-builder-sql/。

这些用例从编译期概念校验、运行期注册校验到 SQL 查询语义多个层面印证了本文所述的架构设计。

十二、常见问题排查

QUICKSTART.md 给出的排查要点:

  • 构建错误:确保 Emscripten SDK 为最新,并使用emcmake cmake
  • 模块找不到:检查 SpacetimeDB 是否正在运行;
  • 类型错误:C++ 类型必须精确匹配,用uint32_t而不是int
  • 约束冲突:约束由数据库强制执行,主键重复会使 reducer 失败(返回Err触发回滚)。

结语

SpacetimeDB C++ 模块库以“编译期概念校验 + 运行期__preinit__注册 + 名义类型系统 + 多层错误检测”的混合架构,在 WASM 环境约束(无异常、16MB 初始内存、线性初始化)下提供了接近 Rust 绑定体验的类型安全开发流程。配合仓库内的完整示例与测试模块,开发者可以从一个最小聊天模块起步,逐步掌握表约束、范围查询、行级安全、定时执行与过程调用等全部能力,将 C++ 后端逻辑直接搬进数据库内部运行。

【免费下载链接】SpacetimeDBDevelopment at the speed of light项目地址: https://gitcode.com/GitHub_Trending/sp/SpacetimeDB

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询