Files
Jason Larabie 14f79910ee Update C++ module bindings to RawModuleDefV10 (#4461)
# 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
2026-02-28 07:05:50 +00:00

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