Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 197 additions & 0 deletions docs/docs/00100-intro/00300-tutorials/00100-chat-app.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

</TabItem>
<TabItem value="cpp" label="C++">

- 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.

</TabItem>
</Tabs>

Expand Down Expand Up @@ -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).

</TabItem>
<TabItem value="cpp" label="C++">

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 latest
./emsdk activate latest
source ./emsdk_env.sh
```

On Windows, use `emsdk_env.bat` 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/).

</TabItem>
</Tabs>

Expand Down Expand Up @@ -113,6 +139,13 @@ spacetime init --lang csharp quickstart-chat
spacetime init --lang rust quickstart-chat
```

</TabItem>
<TabItem value="cpp" label="C++">

```bash
spacetime init --lang cpp quickstart-chat
```

</TabItem>
</Tabs>

Expand Down Expand Up @@ -143,6 +176,16 @@ cd spacetimedb
spacetime build
```

</TabItem>
<TabItem value="cpp" label="C++">

`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.

</TabItem>
</Tabs>

Expand Down Expand Up @@ -199,6 +242,27 @@ From `spacetimedb`, we import:
- `Identity`, a unique identifier for each user.
- `Timestamp`, a point in time.

</TabItem>
<TabItem value="cpp" label="C++">

Open `spacetimedb/src/lib.cpp` and add the SpacetimeDB header:

```cpp server
#include <spacetimedb.h>

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.

</TabItem>
</Tabs>

Expand Down Expand Up @@ -282,6 +346,30 @@ pub struct Message {
}
```

</TabItem>
<TabItem value="cpp" label="C++">

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<std::string> 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);
```

</TabItem>
</Tabs>

Expand Down Expand Up @@ -365,6 +453,38 @@ fn validate_name(name: String) -> Result<String, String> {
}
```

</TabItem>
<TabItem value="cpp" label="C++">

Add to `spacetimedb/src/lib.cpp`:

```cpp server
Outcome<std::string> validate_name(const std::string& name) {
if (name.empty()) {
return Err<std::string>("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");
}
```

</TabItem>
</Tabs>

Expand Down Expand Up @@ -455,6 +575,31 @@ fn validate_message(text: String) -> Result<String, String> {
}
```

</TabItem>
<TabItem value="cpp" label="C++">

Add to `spacetimedb/src/lib.cpp`:

```cpp server
Outcome<std::string> validate_message(const std::string& text) {
if (text.empty()) {
return Err<std::string>("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();
}
```

</TabItem>
</Tabs>

Expand Down Expand Up @@ -568,6 +713,38 @@ pub fn identity_disconnected(ctx: &ReducerContext) {
}
```

</TabItem>
<TabItem value="cpp" label="C++">

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();
}
```

</TabItem>
</Tabs>

Expand Down Expand Up @@ -604,6 +781,13 @@ spacetime publish --server local --project-path spacetimedb quickstart-chat
spacetime publish --server local --project-path spacetimedb quickstart-chat
```

</TabItem>
<TabItem value="cpp" label="C++">

```bash
spacetime publish --server local --project-path spacetimedb quickstart-chat
```

</TabItem>
</Tabs>

Expand Down Expand Up @@ -635,6 +819,13 @@ spacetime call --server local quickstart-chat SendMessage 'Hello, World!'
spacetime call --server local quickstart-chat send_message 'Hello, World!'
```

</TabItem>
<TabItem value="cpp" label="C++">

```bash
spacetime call --server local quickstart-chat send_message 'Hello, World!'
```

</TabItem>
</Tabs>

Expand Down Expand Up @@ -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
Expand Down
12 changes: 12 additions & 0 deletions smoketests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@
# this is set to true when the --skip-dotnet flag is not passed to the cli,
# and a dotnet installation is detected
HAVE_DOTNET = False
# this is set to true when emscripten is detected
HAVE_EMSCRIPTEN = False

# When we pass --spacetime-login, we are running against a server that requires "real" spacetime logins (rather than `--server-issued-login`).
# This is used to skip tests that don't work with that.
Expand Down Expand Up @@ -69,11 +71,21 @@ def stream(self, value):
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.DEBUG)

def check_emscripten():
"""Check if emscripten is available."""
global HAVE_EMSCRIPTEN
HAVE_EMSCRIPTEN = shutil.which("emcc") is not None

def requires_dotnet(item):
if HAVE_DOTNET:
return item
return unittest.skip("dotnet 8.0 not available")(item)

def requires_emscripten(item):
if HAVE_EMSCRIPTEN:
return item
return unittest.skip("emscripten not available")(item)

def requires_anonymous_login(item):
if USE_SPACETIME_LOGIN:
return unittest.skip("using `spacetime login`")(item)
Expand Down
4 changes: 4 additions & 0 deletions smoketests/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ def main():
print("no suitable dotnet installation found")
exit(1)

smoketests.check_emscripten()
if not smoketests.HAVE_EMSCRIPTEN:
logging.info("emscripten not available, skipping C++ smoketests")

add_prefix = lambda testlist: [TESTPREFIX + test for test in testlist]
import fnmatch
excludelist = add_prefix(args.exclude)
Expand Down
45 changes: 45 additions & 0 deletions smoketests/tests/quickstart.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,3 +376,48 @@ def server_postprocess(self, server_path: Path):
def test_quickstart(self):
"""Run the TypeScript quickstart guides for server."""
self._test_quickstart()


class Cpp(Rust):
"""C++ server with Rust client quickstart test."""
lang = "cpp"
client_lang = "rust"
server_doc = STDB_DIR / "docs/docs/00100-intro/00300-tutorials/00100-chat-app.md"
server_file = "src/lib.cpp"
# Inherit client_file, module_bindings, run_cmd, build_cmd from Rust

# Inherit Rust client behavior
replacements = Rust.replacements
extra_code = Rust.extra_code
connected_str = Rust.connected_str

def generate_server(self, server_path: Path):
"""Generate the C++ server code from documentation.
Override to use correct project path and skip Rust-specific setup."""
logging.info(f"Generating server code {self.lang}: {server_path}...")
self.spacetime(
"init",
"--non-interactive",
"--lang",
self.lang,
"--project-path",
server_path,
"spacetimedb-project",
capture_stderr=True,
)
# C++ init creates server_path/spacetimedb structure
self.project_path = server_path / "spacetimedb"
# Don't copy rust-toolchain.toml for C++
_write_file(self.project_path / self.server_file, _parse_quickstart(self.server_doc, self.lang, self._module_name, server=True))
self.server_postprocess(self.project_path)
self.spacetime("build", "-d", "-p", self.project_path, capture_stderr=True)

def server_postprocess(self, server_path: Path):
"""C++ doesn't need Cargo.toml - override parent's Rust implementation."""
pass

def test_quickstart(self):
"""Run the C++ server + Rust client quickstart."""
if not smoketests.HAVE_EMSCRIPTEN:
self.skipTest("C++ SDK requires Emscripten to be installed.")
self._test_quickstart()
Loading