mirror of
https://github.com/clockworklabs/SpacetimeDB.git
synced 2026-05-10 17:49:49 -04:00
14f79910ee
# Description of Changes
- Migrated the C++ module-definition assembly path to V10-first
internals:
- Added v10_builder and module_type_registration systems.
- Switched Module::__describe_module__ to serialize RawModuleDef with
V10 payload.
- Updated macro registration pipeline to register through V10
- Added explicit naming support across macro surface (*_NAMED variants
for reducer/procedure/
view and field/index macros).
- Reworked multi-column index macros (FIELD_MultiColumnIndex,
FIELD_MultiColumnIndex_NAMED) with
migration alias.
- Added SPACETIMEDB_SETTING_CASE_CONVERSION(...) to support case
conversion policy
- Error-path hardening by adding explicit constraint-registration error
tracking and preinit validation
- Codegen updates:
- Updated C++ moduledef regen to V10 builder types.
- Adjusted C++ codegen duplicate-variant wrapper generation to emit
proper product-type
wrappers.
- Test/harness updates:
- type-isolation-test runner now defaults to focused V10 regression
checks; --v9 runs broader
legacy/full suite.
- Added focused modules for positive/negative V10 checks:
- test_multicolumn_index_valid
- error_multicolumn_missing_field
- error_default_missing_field
- Re-enabled C++ paths in sdks/rust/tests/test.rs procedure/view/test
suites.
# API and ABI breaking changes
- Refactor of the underlying module definition
- New *_NAMED variant macros for explicit canonical naming
- FIELD_NamedMultiColumnIndex renamed to FIELD_MultiColumnIndex
# Expected complexity level and risk
3 - Large set of changes moving over to V10 with underlying changes to
make future updates a little easier
# Testing
- [x] Ran the type isolation test and expanded it
- [x] Ran the spacetimedb-sdk test framework to confirm no more drift
between C++ and other module languages
- [x] Ran Unreal test suite though not really applicable
- [x] New app creation with `spacetime init --template basic-cpp`
- [x] Ran describe module tests against Rust + C# matching with C++ on
the /modules/sdk-test* modules to find any possible mis-alignment
# Review
- [x] Another look at the new features with C++
- [x] Thoughts on *_NAMED macros, I couldn't come up with a better
solution with C++20
496 lines
18 KiB
C++
496 lines
18 KiB
C++
#ifndef SPACETIMEDB_MODULE_IMPL_H
|
|
#define SPACETIMEDB_MODULE_IMPL_H
|
|
|
|
/**
|
|
* SpacetimeDB C++ bindings - Module Implementation
|
|
*
|
|
* Core module system implementation with optimized type handling,
|
|
* table registration, and reducer management.
|
|
*
|
|
* Architecture:
|
|
* - Type traits for compile-time type detection
|
|
* - BSATN serialization utilities
|
|
* - Unified table and reducer registration
|
|
* - V9 builder integration for constraints
|
|
*/
|
|
|
|
#include "Module.h"
|
|
#include "field_registration.h"
|
|
#include "../table_with_constraints.h"
|
|
#include "../outcome.h"
|
|
#include <spacetimedb/abi/FFI.h>
|
|
#include "bsatn_adapters.h"
|
|
#include <spacetimedb/bsatn/algebraic_type.h>
|
|
#include <spacetimedb/bsatn/traits.h>
|
|
#include <spacetimedb/bsatn/type_extensions.h>
|
|
#include "autogen/Lifecycle.g.h"
|
|
#include "v10_builder.h"
|
|
#include <cstring>
|
|
#include <cstdio>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <optional>
|
|
#include <functional>
|
|
#include <utility>
|
|
|
|
namespace SpacetimeDB {
|
|
namespace Internal {
|
|
|
|
// =============================================================================
|
|
// TYPE TRAITS
|
|
// =============================================================================
|
|
|
|
// Detect unit structs
|
|
template<typename T>
|
|
struct is_unit_struct_type {
|
|
private:
|
|
template<typename U>
|
|
static auto test(int) -> decltype(U::__is_unit_type__, std::bool_constant<U::__is_unit_type__>{});
|
|
template<typename>
|
|
static std::false_type test(...);
|
|
public:
|
|
static constexpr bool value = decltype(test<T>(0))::value;
|
|
};
|
|
|
|
template<typename T>
|
|
inline constexpr bool is_unit_struct_v = is_unit_struct_type<T>::value;
|
|
|
|
// Detect vector types
|
|
template<typename T>
|
|
struct is_vector_type : std::false_type {};
|
|
|
|
template<typename T, typename Alloc>
|
|
struct is_vector_type<std::vector<T, Alloc>> : std::true_type {};
|
|
|
|
// Detect optional types
|
|
template<typename T>
|
|
struct is_optional : std::false_type {};
|
|
|
|
template<typename T>
|
|
struct is_optional<std::optional<T>> : std::true_type {};
|
|
|
|
// Check for big integer types
|
|
template<typename T>
|
|
constexpr bool is_big_integer_v = std::is_same_v<T, ::SpacetimeDB::u128> ||
|
|
std::is_same_v<T, ::SpacetimeDB::i128> ||
|
|
std::is_same_v<T, ::SpacetimeDB::u256> ||
|
|
std::is_same_v<T, ::SpacetimeDB::i256>;
|
|
|
|
// Check if type needs registry registration
|
|
template<typename T>
|
|
constexpr bool needs_type_registration_v = (!std::is_arithmetic_v<T> &&
|
|
!std::is_same_v<T, std::string> &&
|
|
!is_big_integer_v<T>) ||
|
|
requires { bsatn::bsatn_traits<T>::algebraic_type(); };
|
|
|
|
// Get BSATN tag for primitive types
|
|
template<typename T>
|
|
struct type_id {
|
|
static constexpr uint8_t value = []() {
|
|
if constexpr (std::is_same_v<T, bool>) return 4;
|
|
else if constexpr (std::is_same_v<T, uint8_t>) return 5;
|
|
else if constexpr (std::is_same_v<T, int8_t>) return 6;
|
|
else if constexpr (std::is_same_v<T, uint16_t>) return 7;
|
|
else if constexpr (std::is_same_v<T, int16_t>) return 8;
|
|
else if constexpr (std::is_same_v<T, uint32_t>) return 9;
|
|
else if constexpr (std::is_same_v<T, int32_t>) return 10;
|
|
else if constexpr (std::is_same_v<T, uint64_t>) return 11;
|
|
else if constexpr (std::is_same_v<T, int64_t>) return 12;
|
|
else if constexpr (std::is_same_v<T, float>) return 17;
|
|
else if constexpr (std::is_same_v<T, double>) return 18;
|
|
else if constexpr (std::is_same_v<T, std::string>) return 19;
|
|
else return 0; // Complex type
|
|
}();
|
|
};
|
|
|
|
// Check for BSATN traits
|
|
template<typename T, typename = void>
|
|
struct has_bsatn_traits : std::false_type {};
|
|
|
|
template<typename T>
|
|
struct has_bsatn_traits<T, std::void_t<decltype(bsatn::bsatn_traits<T>::deserialize(std::declval<bsatn::Reader&>()))>> : std::true_type {};
|
|
|
|
template<typename T>
|
|
constexpr bool has_bsatn_traits_v = has_bsatn_traits<T>::value;
|
|
|
|
// Check if type can be written inline
|
|
template<typename T>
|
|
constexpr bool is_basic_inlineable_v =
|
|
std::is_arithmetic_v<T> || std::is_same_v<T, std::string> ||
|
|
std::is_same_v<T, Identity> || std::is_same_v<T, ConnectionId> ||
|
|
std::is_same_v<T, Timestamp> || std::is_same_v<T, TimeDuration> ||
|
|
std::is_same_v<T, u128> || std::is_same_v<T, u256> ||
|
|
std::is_same_v<T, i128> || std::is_same_v<T, i256>;
|
|
|
|
// =============================================================================
|
|
// BINARY I/O UTILITIES
|
|
// =============================================================================
|
|
|
|
inline void write_u32(std::vector<uint8_t>& buf, uint32_t val) {
|
|
bsatn::Writer writer(buf);
|
|
writer.write_u32_le(val);
|
|
}
|
|
|
|
inline void write_string(std::vector<uint8_t>& buf, const std::string& str) {
|
|
bsatn::Writer writer(buf);
|
|
writer.write_string(str);
|
|
}
|
|
|
|
inline uint8_t read_u8(uint32_t source) {
|
|
BytesSource src{source};
|
|
BytesSourceReader reader(src);
|
|
return reader.read_u8();
|
|
}
|
|
|
|
inline uint32_t read_u32(uint32_t source) {
|
|
BytesSource src{source};
|
|
BytesSourceReader reader(src);
|
|
return reader.read_u32_le();
|
|
}
|
|
|
|
// =============================================================================
|
|
// TABLE REGISTRATION
|
|
// =============================================================================
|
|
|
|
// Unified table registration - single implementation
|
|
template<typename T>
|
|
void Module::RegisterTableInternalImpl(const char* name, bool is_public, bool is_event) {
|
|
// V10 registration entrypoint.
|
|
SetTableIsEventFlag(name, is_event);
|
|
getV10Builder().RegisterTable<T>(name, is_public, is_event);
|
|
}
|
|
|
|
// =============================================================================
|
|
// ARGUMENT DESERIALIZATION
|
|
// =============================================================================
|
|
|
|
template<typename T>
|
|
T read_arg(uint32_t source) {
|
|
BytesSource src{source};
|
|
|
|
if constexpr (is_unit_struct_v<T>) {
|
|
return T{};
|
|
} else if constexpr (!needs_type_registration_v<T>) {
|
|
BytesSourceReader reader(src);
|
|
if constexpr (std::is_same_v<T, bool>) {
|
|
return reader.read_bool();
|
|
} else if constexpr (std::is_same_v<T, uint8_t>) {
|
|
return reader.read_u8();
|
|
} else if constexpr (std::is_same_v<T, uint16_t>) {
|
|
return reader.read_u16_le();
|
|
} else if constexpr (std::is_same_v<T, uint32_t>) {
|
|
return reader.read_u32_le();
|
|
} else if constexpr (std::is_same_v<T, uint64_t>) {
|
|
return reader.read_u64_le();
|
|
} else if constexpr (std::is_same_v<T, int8_t>) {
|
|
return reader.read_i8();
|
|
} else if constexpr (std::is_same_v<T, int16_t>) {
|
|
return reader.read_i16_le();
|
|
} else if constexpr (std::is_same_v<T, int32_t>) {
|
|
return reader.read_i32_le();
|
|
} else if constexpr (std::is_same_v<T, int64_t>) {
|
|
return reader.read_i64_le();
|
|
} else if constexpr (std::is_same_v<T, float>) {
|
|
return reader.read_f32_le();
|
|
} else if constexpr (std::is_same_v<T, double>) {
|
|
return reader.read_f64_le();
|
|
} else if constexpr (std::is_same_v<T, std::string>) {
|
|
return reader.read_string();
|
|
} else {
|
|
static_assert(!sizeof(T), "Unsupported primitive type");
|
|
}
|
|
} else if constexpr (has_bsatn_traits_v<T>) {
|
|
std::vector<uint8_t> buffer;
|
|
constexpr size_t CHUNK_SIZE = 256;
|
|
uint8_t chunk[CHUNK_SIZE];
|
|
|
|
while (true) {
|
|
size_t requested = CHUNK_SIZE;
|
|
size_t actual = requested;
|
|
FFI::bytes_source_read(src, chunk, &actual);
|
|
|
|
if (actual == 0) break;
|
|
buffer.insert(buffer.end(), chunk, chunk + actual);
|
|
if (actual < requested) break;
|
|
}
|
|
|
|
bsatn::Reader reader(buffer);
|
|
return bsatn::bsatn_traits<T>::deserialize(reader);
|
|
} else {
|
|
static_assert(std::is_default_constructible_v<T>,
|
|
"Type must be default constructible or have BSATN traits");
|
|
return T{};
|
|
}
|
|
}
|
|
|
|
// Read multiple arguments as tuple
|
|
template<typename... Args>
|
|
auto read_args_tuple(uint32_t args_source) {
|
|
BytesSource src{args_source};
|
|
|
|
// Handle single unit struct specially
|
|
if constexpr (sizeof...(Args) == 1) {
|
|
using FirstArg = std::tuple_element_t<0, std::tuple<Args...>>;
|
|
if constexpr (is_unit_struct_v<FirstArg>) {
|
|
return std::make_tuple(FirstArg{});
|
|
}
|
|
}
|
|
|
|
if constexpr ((has_bsatn_traits_v<Args> || ...)) {
|
|
std::vector<uint8_t> buffer;
|
|
constexpr size_t CHUNK_SIZE = 256;
|
|
uint8_t chunk[CHUNK_SIZE];
|
|
|
|
while (true) {
|
|
size_t requested = CHUNK_SIZE;
|
|
size_t actual = requested;
|
|
FFI::bytes_source_read(src, chunk, &actual);
|
|
|
|
if (actual == 0) break;
|
|
buffer.insert(buffer.end(), chunk, chunk + actual);
|
|
if (actual < requested) break;
|
|
}
|
|
|
|
bsatn::Reader reader(buffer);
|
|
return std::make_tuple(bsatn::bsatn_traits<Args>::deserialize(reader)...);
|
|
} else {
|
|
BytesSourceReader reader(src);
|
|
return std::make_tuple([&reader]() -> Args {
|
|
if constexpr (std::is_same_v<Args, uint32_t>) {
|
|
return reader.read_u32_le();
|
|
} else if constexpr (std::is_same_v<Args, int32_t>) {
|
|
return reader.read_i32_le();
|
|
} else if constexpr (std::is_same_v<Args, uint64_t>) {
|
|
return reader.read_u64_le();
|
|
} else if constexpr (std::is_same_v<Args, int64_t>) {
|
|
return reader.read_i64_le();
|
|
} else if constexpr (std::is_same_v<Args, bool>) {
|
|
return reader.read_bool();
|
|
} else if constexpr (std::is_same_v<Args, std::string>) {
|
|
return reader.read_string();
|
|
} else {
|
|
return bsatn::bsatn_traits<Args>::deserialize(reader);
|
|
}
|
|
}()...);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// REDUCER WRAPPERS
|
|
// =============================================================================
|
|
|
|
// Lifecycle reducer wrapper
|
|
template<typename Func>
|
|
void builtin_reducer_wrapper(Func func, ReducerContext& ctx,
|
|
uint64_t sender_0, uint64_t sender_1,
|
|
uint64_t sender_2, uint64_t sender_3) {
|
|
std::array<uint8_t, 32> senderBytes{};
|
|
memcpy(senderBytes.data(), &sender_0, sizeof(uint64_t));
|
|
memcpy(senderBytes.data() + 8, &sender_1, sizeof(uint64_t));
|
|
memcpy(senderBytes.data() + 16, &sender_2, sizeof(uint64_t));
|
|
memcpy(senderBytes.data() + 24, &sender_3, sizeof(uint64_t));
|
|
Identity sender(senderBytes);
|
|
|
|
if constexpr (std::is_invocable_v<Func, ReducerContext>) {
|
|
func(ctx);
|
|
} else if constexpr (std::is_invocable_v<Func, ReducerContext, Identity>) {
|
|
func(ctx, sender);
|
|
}
|
|
}
|
|
|
|
// Generic reducer wrapper
|
|
template<typename... Args>
|
|
void spacetimedb_reducer_wrapper(void (*func)(ReducerContext, Args...),
|
|
ReducerContext& ctx, uint32_t args_source) {
|
|
if constexpr (sizeof...(Args) == 0) {
|
|
func(ctx);
|
|
} else {
|
|
auto args_tuple = read_args_tuple<Args...>(args_source);
|
|
std::apply([&](auto&&... args) {
|
|
func(ctx, std::forward<decltype(args)>(args)...);
|
|
}, args_tuple);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// TYPE SERIALIZATION
|
|
// =============================================================================
|
|
|
|
// Write algebraic type inline (for special types and primitives)
|
|
template<typename T>
|
|
void write_algebraic_type_inline(std::vector<uint8_t>& buf) {
|
|
if constexpr (is_optional<T>::value) {
|
|
using inner_type = typename T::value_type;
|
|
|
|
buf.push_back(1); // AlgebraicType::Sum
|
|
write_u32(buf, 2); // 2 variants
|
|
|
|
buf.push_back(0); // Has name
|
|
write_string(buf, "some");
|
|
write_algebraic_type_inline<inner_type>(buf);
|
|
|
|
buf.push_back(0); // Has name
|
|
write_string(buf, "none");
|
|
buf.push_back(2); // AlgebraicType::Product (empty)
|
|
write_u32(buf, 0); // 0 elements
|
|
|
|
} else if constexpr (is_vector_type<T>::value) {
|
|
using element_type = typename T::value_type;
|
|
buf.push_back(3); // AlgebraicType::Array
|
|
write_algebraic_type_inline<element_type>(buf);
|
|
|
|
} else if constexpr (std::is_same_v<T, int32_t>) {
|
|
buf.push_back(10);
|
|
} else if constexpr (std::is_same_v<T, uint32_t>) {
|
|
buf.push_back(11);
|
|
} else if constexpr (std::is_same_v<T, std::string>) {
|
|
buf.push_back(4);
|
|
} else if constexpr (std::is_same_v<T, bool>) {
|
|
buf.push_back(5);
|
|
} else if constexpr (std::is_same_v<T, int8_t>) {
|
|
buf.push_back(6);
|
|
} else if constexpr (std::is_same_v<T, uint8_t>) {
|
|
buf.push_back(7);
|
|
} else if constexpr (std::is_same_v<T, int16_t>) {
|
|
buf.push_back(8);
|
|
} else if constexpr (std::is_same_v<T, uint16_t>) {
|
|
buf.push_back(9);
|
|
} else if constexpr (std::is_same_v<T, int64_t>) {
|
|
buf.push_back(12);
|
|
} else if constexpr (std::is_same_v<T, uint64_t>) {
|
|
buf.push_back(13);
|
|
} else if constexpr (std::is_same_v<T, float>) {
|
|
buf.push_back(18);
|
|
} else if constexpr (std::is_same_v<T, double>) {
|
|
buf.push_back(19);
|
|
} else if constexpr (std::is_same_v<T, u128>) {
|
|
buf.push_back(15);
|
|
} else if constexpr (std::is_same_v<T, u256>) {
|
|
buf.push_back(17);
|
|
} else if constexpr (std::is_same_v<T, i128>) {
|
|
buf.push_back(14);
|
|
} else if constexpr (std::is_same_v<T, i256>) {
|
|
buf.push_back(16);
|
|
} else if constexpr (std::is_same_v<T, Identity>) {
|
|
buf.push_back(2); // AlgebraicType::Product
|
|
write_u32(buf, 1); // 1 field
|
|
buf.push_back(0); // Some (has name)
|
|
write_string(buf, bsatn::IDENTITY_TAG);
|
|
buf.push_back(17); // AlgebraicType::U256
|
|
} else if constexpr (std::is_same_v<T, ConnectionId>) {
|
|
buf.push_back(2); // AlgebraicType::Product
|
|
write_u32(buf, 1); // 1 field
|
|
buf.push_back(0); // Some (has name)
|
|
write_string(buf, bsatn::CONNECTION_ID_TAG);
|
|
buf.push_back(15); // AlgebraicType::U128
|
|
} else if constexpr (std::is_same_v<T, Timestamp>) {
|
|
buf.push_back(2); // AlgebraicType::Product
|
|
write_u32(buf, 1); // 1 field
|
|
buf.push_back(0); // Some (has name)
|
|
write_string(buf, bsatn::TIMESTAMP_TAG);
|
|
buf.push_back(12); // AlgebraicType::I64
|
|
} else if constexpr (std::is_same_v<T, TimeDuration>) {
|
|
buf.push_back(2); // AlgebraicType::Product
|
|
write_u32(buf, 1); // 1 field
|
|
buf.push_back(0); // Some (has name)
|
|
write_string(buf, bsatn::TIME_DURATION_TAG);
|
|
buf.push_back(12); // AlgebraicType::I64
|
|
} else if constexpr (std::is_enum_v<T>) {
|
|
buf.push_back(4); // String fallback for complex enums
|
|
} else {
|
|
buf.push_back(4); // AlgebraicType::String fallback
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// REDUCER REGISTRATION
|
|
// =============================================================================
|
|
|
|
inline std::optional<Lifecycle> get_lifecycle_for_name(const std::string& name) {
|
|
if (name == "init") return Lifecycle::Init;
|
|
if (name == "client_connected") return Lifecycle::OnConnect;
|
|
if (name == "client_disconnected") return Lifecycle::OnDisconnect;
|
|
return std::nullopt;
|
|
}
|
|
|
|
template<typename Func>
|
|
void Module::RegisterReducerInternalImpl(const std::string& name, Func func) {
|
|
auto lifecycle = get_lifecycle_for_name(name);
|
|
if (lifecycle.has_value()) {
|
|
getV10Builder().RegisterLifecycleReducer(name, func, lifecycle.value());
|
|
} else {
|
|
getV10Builder().RegisterReducer(name, func, std::vector<std::string>{});
|
|
}
|
|
}
|
|
|
|
template<typename Func>
|
|
void Module::RegisterReducerInternalWithNames(const std::string& name, Func func, const std::vector<std::string>& param_names) {
|
|
auto lifecycle = get_lifecycle_for_name(name);
|
|
if (lifecycle.has_value()) {
|
|
getV10Builder().RegisterLifecycleReducer(name, func, lifecycle.value());
|
|
} else {
|
|
getV10Builder().RegisterReducer(name, func, param_names);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// HELPER FUNCTIONS
|
|
// =============================================================================
|
|
|
|
template<typename T>
|
|
void register_table_type(const char* name, bool is_public) {
|
|
Module::RegisterTableInternalImpl<T>(name, is_public);
|
|
}
|
|
|
|
// Optimized parameter name parsing
|
|
inline std::vector<std::string> parse_parameter_names(const std::string& params_str) {
|
|
std::vector<std::string> names;
|
|
|
|
size_t pos = 0;
|
|
bool first_param = true;
|
|
|
|
while (pos < params_str.length()) {
|
|
size_t comma_pos = params_str.find(',', pos);
|
|
if (comma_pos == std::string::npos) comma_pos = params_str.length();
|
|
|
|
std::string param = params_str.substr(pos, comma_pos - pos);
|
|
|
|
// Skip ReducerContext
|
|
if (first_param) {
|
|
first_param = false;
|
|
pos = comma_pos + 1;
|
|
continue;
|
|
}
|
|
|
|
// Trim whitespace
|
|
size_t start = param.find_first_not_of(" \t");
|
|
if (start != std::string::npos) {
|
|
param = param.substr(start);
|
|
size_t end = param.find_last_not_of(" \t");
|
|
if (end != std::string::npos) {
|
|
param = param.substr(0, end + 1);
|
|
}
|
|
}
|
|
|
|
// Extract parameter name
|
|
size_t last_space = param.find_last_of(" \t");
|
|
if (last_space != std::string::npos) {
|
|
std::string param_name = param.substr(last_space + 1);
|
|
size_t name_end = param_name.find_first_of("&*[]");
|
|
if (name_end != std::string::npos) {
|
|
param_name = param_name.substr(0, name_end);
|
|
}
|
|
names.push_back(std::move(param_name));
|
|
}
|
|
|
|
pos = comma_pos + 1;
|
|
}
|
|
|
|
return names;
|
|
}
|
|
|
|
} // namespace Internal
|
|
} // namespace SpacetimeDB
|
|
|
|
#endif // SPACETIMEDB_MODULE_IMPL_H
|