Files
SpacetimeDB/modules/sdk-test-view-cpp/src/lib.cpp
T
Jason Larabie 52b6c66fa1 Add C++ Bindings (#3544)
# Description of Changes

This adds C++ server bindings (/crate/bindings-cpp) to allow writing C++
20 modules.

- Emscripten WASM build system integration with CMake
- Macro-based code generation (SPACETIMEDB_TABLE, SPACETIMEDB_REDUCER,
etc)
- All SpacetimeDB types supported (primitives, Timestamp, Identity,
Uuid, etc)
- Product types via SPACETIMEDB_STRUCT
- Sum types via SPACETIMEDB_ENUM
- Constraints marked with FIELD* macros

# API and ABI breaking changes

None

# Expected complexity level and risk

2 - Doesn't heavily impact any other areas but is complex macro C++
structure to support a similar developer experience, did have a small
impact on init command

# Testing

- [x] modules/module-test-cpp - heavily tested every reducer
- [x] modules/benchmarks-cpp - tested through the standalone (~6x faster
than C#, ~6x slower than Rust)
- [x] modules/sdk-test-cpp
- [x] modules/sdk-test-procedure-cpp
- [x] modules/sdk-test-view-cpp  
- [x] Wrote several test modules myself
- [x] Quickstart smoketest [Currently in progress]
- [ ] Write Blackholio C++ server module

---------

Signed-off-by: Jason Larabie <jason@clockworklabs.io>
Co-authored-by: clockwork-labs-bot <clockwork-labs-bot@users.noreply.github.com>
Co-authored-by: Ryan <r.ekhoff@clockworklabs.io>
Co-authored-by: John Detter <4099508+jdetter@users.noreply.github.com>
2026-02-07 04:26:45 +00:00

218 lines
6.5 KiB
C++

#include <spacetimedb.h>
#include <optional>
#include <vector>
#include <cmath>
using namespace SpacetimeDB;
// =============================================================================
// C++ bindings View Test Module - Mirrors Rust sdk-test-view
// =============================================================================
//
// This module provides a complete C++ implementation of the Rust sdk-test-view
// module, testing all view functionality including:
// - ViewContext views (with sender identity)
// - AnonymousViewContext views (without sender)
// - Optional and Vec return types
// - Joins across multiple tables
// - Filtering and complex queries
//
// NOTE: This module has NO INIT function. Test data is created dynamically
// by the test client via the reducers.
// =============================================================================
// Table: player
struct Player {
uint64_t entity_id;
Identity identity;
};
SPACETIMEDB_STRUCT(Player, entity_id, identity)
SPACETIMEDB_TABLE(Player, player, Public)
FIELD_PrimaryKey(player, entity_id);
FIELD_AutoInc(player, entity_id);
FIELD_Unique(player, identity);
// Table: player_level
struct PlayerLevel {
uint64_t entity_id;
uint64_t level;
};
SPACETIMEDB_STRUCT(PlayerLevel, entity_id, level)
SPACETIMEDB_TABLE(PlayerLevel, player_level, Public)
FIELD_Unique(player_level, entity_id);
FIELD_Index(player_level, level);
// Table: player_location
struct PlayerLocation {
uint64_t entity_id;
bool active;
int32_t x;
int32_t y;
};
SPACETIMEDB_STRUCT(PlayerLocation, entity_id, active, x, y)
SPACETIMEDB_TABLE(PlayerLocation, player_location, Public)
FIELD_Unique(player_location, entity_id);
FIELD_Index(player_location, active);
// Custom type for joined results
struct PlayerAndLevel {
uint64_t entity_id;
Identity identity;
uint64_t level;
};
SPACETIMEDB_STRUCT(PlayerAndLevel, entity_id, identity, level)
// =============================================================================
// REDUCERS
// =============================================================================
SPACETIMEDB_REDUCER(insert_player, ReducerContext ctx, Identity identity, uint64_t level)
{
// Insert player and get the entity_id
Player player_result = ctx.db[player].insert(Player{0, identity});
// Insert player level
ctx.db[player_level].insert(PlayerLevel{player_result.entity_id, level});
return Ok();
}
SPACETIMEDB_REDUCER(delete_player, ReducerContext ctx, Identity identity)
{
// Find player by identity using index
auto player_opt = ctx.db[player_identity].find(identity);
if (player_opt.has_value()) {
uint64_t eid = player_opt->entity_id;
// Delete from both tables
ctx.db[player_entity_id].delete_by_key(eid);
ctx.db[player_level_entity_id].delete_by_value(eid);
}
return Ok();
}
SPACETIMEDB_REDUCER(move_player, ReducerContext ctx, int32_t dx, int32_t dy)
{
// Find or create player
auto my_player_opt = ctx.db[player_identity].find(ctx.sender);
Player my_player;
if (!my_player_opt.has_value()) {
// Create new player
my_player = ctx.db[player].insert(Player{0, ctx.sender});
} else {
my_player = my_player_opt.value();
}
// Find or create location
auto loc_opt = ctx.db[player_location_entity_id].find(my_player.entity_id);
if (loc_opt.has_value()) {
// Update existing location
PlayerLocation updated = loc_opt.value();
updated.x += dx;
updated.y += dy;
ctx.db[player_location_entity_id].update(updated);
} else {
// Insert new location
ctx.db[player_location].insert(PlayerLocation{
my_player.entity_id,
true,
dx,
dy
});
}
return Ok();
}
// =============================================================================
// VIEWS
// =============================================================================
// View: my_player - Returns the player for the caller
SPACETIMEDB_VIEW(std::optional<Player>, my_player, Public, ViewContext ctx) {
auto player_opt = ctx.db[player_identity].find(ctx.sender);
return player_opt;
}
// View: my_player_and_level - Returns player with level joined
SPACETIMEDB_VIEW(std::optional<PlayerAndLevel>, my_player_and_level, Public, ViewContext ctx) {
// Find the caller's player
auto player_opt = ctx.db[player_identity].find(ctx.sender);
if (!player_opt.has_value()) {
return std::optional<PlayerAndLevel>();
}
Player p = player_opt.value();
// Find the player's level
auto level_opt = ctx.db[player_level_entity_id].find(p.entity_id);
if (!level_opt.has_value()) {
return std::optional<PlayerAndLevel>();
}
// Combine into result
PlayerAndLevel result{
p.entity_id,
p.identity,
level_opt->level
};
return std::optional<PlayerAndLevel>(result);
}
// View: players_at_level_0 - Returns all players at level 0 (anonymous)
SPACETIMEDB_VIEW(std::vector<Player>, players_at_level_0, Public, AnonymousViewContext ctx) {
std::vector<Player> results;
// Find all players at level 0
for (const auto& lvl : ctx.db[player_level_level].filter(0ULL)) {
auto player_opt = ctx.db[player_entity_id].find(lvl.entity_id);
if (player_opt.has_value()) {
results.push_back(player_opt.value());
}
}
return results;
}
// View: nearby_players - Returns players within 5 units
SPACETIMEDB_VIEW(std::vector<PlayerLocation>, nearby_players, Public, ViewContext ctx) {
std::vector<PlayerLocation> results;
// Find the caller's player
auto my_player_opt = ctx.db[player_identity].find(ctx.sender);
if (!my_player_opt.has_value()) {
return results; // No player, return empty
}
// Find the caller's location
auto my_loc_opt = ctx.db[player_location_entity_id].find(my_player_opt->entity_id);
if (!my_loc_opt.has_value()) {
return results; // No location, return empty
}
PlayerLocation my_loc = my_loc_opt.value();
// Find all active players
for (const auto& loc : ctx.db[player_location_active].filter(true)) {
// Skip self
if (loc.entity_id == my_loc.entity_id) {
continue;
}
// Check if within range
int32_t dx = std::abs(loc.x - my_loc.x);
int32_t dy = std::abs(loc.y - my_loc.y);
if (dx < 5 && dy < 5) {
results.push_back(loc);
}
}
return results;
}