diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 69441f9afa5..d2b87f14664 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,25 @@ jobs: with: run_install: true + # Install emscripten for C++ module compilation tests. + - name: Install emscripten (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + git clone https://github.com/emscripten-core/emsdk.git ~/emsdk + cd ~/emsdk + ./emsdk install 4.0.21 + ./emsdk activate 4.0.21 + + - name: Install emscripten (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + git clone https://github.com/emscripten-core/emsdk.git $env:USERPROFILE\emsdk + cd $env:USERPROFILE\emsdk + .\emsdk install 4.0.21 + .\emsdk activate 4.0.21 + - name: Install psql (Windows) if: runner.os == 'Windows' run: choco install psql -y --no-progress @@ -122,11 +141,27 @@ jobs: - name: Install cargo-nextest uses: taiki-e/install-action@nextest - - name: Run smoketests - # --test-threads=1 eliminates contention in the C# tests where they fight over bindings - # build artifacts. - # It also seemed to improve performance a fair amount (11m -> 6m) - run: cargo ci smoketests -- --test-threads=1 + # --test-threads=1 eliminates contention in the C# tests where they fight over bindings + # build artifacts. + # It also seemed to improve performance a fair amount (11m -> 6m) + - name: Run smoketests (Linux) + if: runner.os == 'Linux' + shell: bash + run: | + if [ -f ~/emsdk/emsdk_env.sh ]; then + source ~/emsdk/emsdk_env.sh + fi + cargo ci smoketests -- --test-threads=1 + + # Due to Emscripten PATH issues this was separated to make sure OpenSSL still builds correctly + - name: Run smoketests (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + if (Test-Path "$env:USERPROFILE\emsdk\emsdk_env.ps1") { + & "$env:USERPROFILE\emsdk\emsdk_env.ps1" | Out-Null + } + cargo ci smoketests -- --test-threads=1 smoketests-python: needs: [lints] diff --git a/crates/smoketests/src/lib.rs b/crates/smoketests/src/lib.rs index d69b2ee115e..580bae32374 100644 --- a/crates/smoketests/src/lib.rs +++ b/crates/smoketests/src/lib.rs @@ -139,6 +139,15 @@ macro_rules! require_pnpm { }; } +#[macro_export] +macro_rules! require_emscripten { + () => { + if !$crate::have_emscripten() { + panic!("emcc (Emscripten) not found"); + } + }; +} + /// Helper macro for timing operations and printing results macro_rules! timed { ($label:expr, $expr:expr) => {{ @@ -242,6 +251,12 @@ pub fn pnpm_path() -> Option { PNPM_PATH.get_or_init(|| which("pnpm").ok()).clone() } +/// Returns true if Emscripten (emcc) is available on the system. +pub fn have_emscripten() -> bool { + static HAVE_EMSCRIPTEN: OnceLock = OnceLock::new(); + *HAVE_EMSCRIPTEN.get_or_init(|| which("emcc").is_ok() || which("emcc.bat").is_ok()) +} + /// A smoketest instance that manages a SpacetimeDB server and module project. pub struct Smoketest { /// The SpacetimeDB server guard (stops server on drop). diff --git a/crates/smoketests/tests/quickstart.rs b/crates/smoketests/tests/quickstart.rs index 6e9b91985ef..5d5b5b5683d 100644 --- a/crates/smoketests/tests/quickstart.rs +++ b/crates/smoketests/tests/quickstart.rs @@ -4,7 +4,7 @@ use anyhow::{bail, Context, Result}; use regex::Regex; -use spacetimedb_smoketests::{pnpm_path, require_dotnet, require_pnpm, workspace_root, Smoketest}; +use spacetimedb_smoketests::{pnpm_path, require_dotnet, require_emscripten, require_pnpm, workspace_root, Smoketest}; use std::fs; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; @@ -438,6 +438,37 @@ fn user_input_direct(ctx: &DbConnection) { std::thread::sleep(std::time::Duration::from_secs(1)); std::process::exit(0); } +"#, + connected_str: "connected", + } + } + + fn cpp() -> Self { + // C++ server uses Rust client (same as TypeScript pattern) + Self { + lang: "cpp", + client_lang: "rust", + server_file: "src/lib.cpp", + client_file: "src/main.rs", + module_bindings: "src/module_bindings", + run_cmd: &["cargo", "run"], + build_cmd: &["cargo", "build"], + replacements: &[ + ("user_input_loop(&ctx)", "user_input_direct(&ctx)"), + (".with_token(creds_store()", "//.with_token(creds_store()"), + ], + extra_code: r#" +fn user_input_direct(ctx: &DbConnection) { + let mut line = String::new(); + std::io::stdin().read_line(&mut line).expect("Failed to read from stdin."); + if let Some(name) = line.strip_prefix("/name ") { + ctx.reducers.set_name(name.to_string()).unwrap(); + } else { + ctx.reducers.send_message(line).unwrap(); + } + std::thread::sleep(std::time::Duration::from_secs(1)); + std::process::exit(0); +} "#, connected_str: "connected", } @@ -782,3 +813,12 @@ fn test_quickstart_typescript() { let mut qt = QuickstartTest::new(QuickstartConfig::typescript()); qt.run_quickstart().expect("TypeScript quickstart test failed"); } + +/// Run the C++ quickstart for server (with Rust client). +#[test] +fn test_quickstart_cpp() { + require_emscripten!(); + + let mut qt = QuickstartTest::new(QuickstartConfig::cpp()); + qt.run_quickstart().expect("C++ quickstart test failed"); +} diff --git a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md index 06ef0c039ce..7c4a91bc579 100644 --- a/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md +++ b/docs/docs/00100-intro/00300-tutorials/00100-chat-app.md @@ -41,6 +41,13 @@ SpacetimeDB runs your module inside the database host (not Node.js). There's no - By default, tables are **private**. The `#[table(name = table_name, public)]` macro makes a table public. **Public** tables are readable by all users but can still only be modified by your server module code. - A reducer is a function that traverses and updates the database. Each reducer call runs in its own transaction, and its updates to the database are only committed if the reducer returns successfully. Reducers may return a `Result<()>`, with an `Err` return aborting the transaction. + + + +- Each table is defined as a C++ struct with the `SPACETIMEDB_STRUCT` macro to register its fields, and the `SPACETIMEDB_TABLE` macro to create the table. An instance of the struct represents a row, and each field represents a column. +- By default, tables are **private**. Use `SPACETIMEDB_TABLE(StructName, table_name, Public)` to make a table public. **Public** tables are readable by all users but can still only be modified by your server module code. +- A reducer is a function defined with the `SPACETIMEDB_REDUCER` macro that traverses and updates the database. Each reducer call runs in its own transaction, and its updates to the database are only committed if the reducer returns successfully. Reducers may return `Err("message")` to abort the transaction. + @@ -85,6 +92,25 @@ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh If you're on Windows, go [here](https://learn.microsoft.com/en-us/windows/dev-environment/rust/setup). + + + +Next we need to [install Emscripten](https://emscripten.org/docs/getting_started/downloads.html) so that we can compile our C++ module to WebAssembly. + +Install the Emscripten SDK: + +```bash +git clone https://github.com/emscripten-core/emsdk.git +cd emsdk +./emsdk install 4.0.21 +./emsdk activate 4.0.21 +source ./emsdk_env.sh +``` + +On Windows, use `emsdk_env.bat` or `emsdk_env.ps1` instead of `source ./emsdk_env.sh`. + +You'll also need CMake installed on your system. On macOS: `brew install cmake`. On Ubuntu/Debian: `sudo apt install cmake`. On Windows, download from [cmake.org](https://cmake.org/download/). + @@ -113,6 +139,13 @@ spacetime init --lang csharp quickstart-chat spacetime init --lang rust quickstart-chat ``` + + + +```bash +spacetime init --lang cpp quickstart-chat +``` + @@ -143,6 +176,16 @@ cd spacetimedb spacetime build ``` + + + +`spacetime init` generates a few files: + +1. `spacetimedb/src/lib.cpp` - your module code +2. `spacetimedb/CMakeLists.txt` - build configuration for Emscripten + +Clear out the example code in `src/lib.cpp` so we can write our chat module. + @@ -199,6 +242,27 @@ From `spacetimedb`, we import: - `Identity`, a unique identifier for each user. - `Timestamp`, a point in time. + + + +Open `spacetimedb/src/lib.cpp` and add the SpacetimeDB header: + +```cpp server +#include + +using namespace SpacetimeDB; +``` + +This gives us access to: + +- `SPACETIMEDB_STRUCT` macro to register struct fields. +- `SPACETIMEDB_TABLE` macro to define SpacetimeDB tables. +- `SPACETIMEDB_REDUCER` macro to define SpacetimeDB reducers. +- `FIELD_*` macros to define primary keys, indexes, and constraints. +- `ReducerContext` passed to each reducer. +- `Identity` for unique user identifiers. +- `Timestamp` for points in time. + @@ -282,6 +346,30 @@ pub struct Message { } ``` + + + +In `spacetimedb/src/lib.cpp`, define the `User` and `Message` structs with the `SPACETIMEDB_STRUCT` macro, then create tables with the `SPACETIMEDB_TABLE` macro: + +```cpp server +struct User { + Identity identity; + std::optional name; + bool online; +}; +SPACETIMEDB_STRUCT(User, identity, name, online); +SPACETIMEDB_TABLE(User, user, Public); +FIELD_PrimaryKey(user, identity); + +struct Message { + Identity sender; + Timestamp sent; + std::string text; +}; +SPACETIMEDB_STRUCT(Message, sender, sent, text); +SPACETIMEDB_TABLE(Message, message, Public); +``` + @@ -365,6 +453,38 @@ fn validate_name(name: String) -> Result { } ``` + + + +Add to `spacetimedb/src/lib.cpp`: + +```cpp server +Outcome validate_name(const std::string& name) { + if (name.empty()) { + return Err("Names must not be empty"); + } + return Ok(name); +} + +SPACETIMEDB_REDUCER(set_name, ReducerContext ctx, std::string name) { + auto validated = validate_name(name); + if (validated.is_err()) { + return Err(validated.error()); + } + + // Find and update the user by identity (primary key) + auto user_row = ctx.db[user_identity].find(ctx.sender); + if (user_row.has_value()) { + auto user = user_row.value(); + user.name = validated.value(); + ctx.db[user_identity].update(user); + return Ok(); + } + + return Err("Cannot set name for unknown user"); +} +``` + @@ -455,6 +575,31 @@ fn validate_message(text: String) -> Result { } ``` + + + +Add to `spacetimedb/src/lib.cpp`: + +```cpp server +Outcome validate_message(const std::string& text) { + if (text.empty()) { + return Err("Messages must not be empty"); + } + return Ok(text); +} + +SPACETIMEDB_REDUCER(send_message, ReducerContext ctx, std::string text) { + auto validated = validate_message(text); + if (validated.is_err()) { + return Err(validated.error()); + } + + Message msg{ctx.sender, ctx.timestamp, validated.value()}; + ctx.db[message].insert(msg); + return Ok(); +} +``` + @@ -568,6 +713,38 @@ pub fn identity_disconnected(ctx: &ReducerContext) { } ``` + + + +Add to `spacetimedb/src/lib.cpp`: + +```cpp server +SPACETIMEDB_CLIENT_CONNECTED(client_connected, ReducerContext ctx) { + auto user_row = ctx.db[user_identity].find(ctx.sender); + if (user_row.has_value()) { + auto user = user_row.value(); + user.online = true; + ctx.db[user_identity].update(user); + } else { + User new_user{ctx.sender, std::nullopt, true}; + ctx.db[user].insert(new_user); + } + return Ok(); +} + +SPACETIMEDB_CLIENT_DISCONNECTED(client_disconnected, ReducerContext ctx) { + auto user_row = ctx.db[user_identity].find(ctx.sender); + if (user_row.has_value()) { + auto user = user_row.value(); + user.online = false; + ctx.db[user_identity].update(user); + } else { + LOG_WARN("Disconnect event for unknown user"); + } + return Ok(); +} +``` + @@ -604,6 +781,13 @@ spacetime publish --server local --project-path spacetimedb quickstart-chat spacetime publish --server local --project-path spacetimedb quickstart-chat ``` + + + +```bash +spacetime publish --server local --project-path spacetimedb quickstart-chat +``` + @@ -635,6 +819,13 @@ spacetime call --server local quickstart-chat SendMessage 'Hello, World!' spacetime call --server local quickstart-chat send_message 'Hello, World!' ``` + + + +```bash +spacetime call --server local quickstart-chat send_message 'Hello, World!' +``` + @@ -665,6 +856,12 @@ You've just set up your first SpacetimeDB module! You can find the full code for - [C# server module](https://github.com/clockworklabs/SpacetimeDB/tree/master/templates/chat-console-cs/spacetimedb) - [Rust server module](https://github.com/clockworklabs/SpacetimeDB/tree/master/templates/chat-console-rs/spacetimedb) +:::note + +For C++ modules, there is not yet a dedicated C++ client SDK. To test your C++ module with a client, use one of the available client libraries: [TypeScript (React)](#creating-the-client), [C# (Console)](#creating-the-client), or [Rust (Console)](#creating-the-client). We recommend starting with the [Rust client](#creating-the-client) for testing C++ modules. + +::: + --- ## Creating the Client