Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .clang-tidy
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@ Checks:
- -cppcoreguidelines-pro-bounds-array-to-pointer-decay
- -cppcoreguidelines-pro-type-static-cast-downcast
- -bugprone-easily-swappable-parameters
- -bugprone-assert-side-effect
- -readability-named-parameter
- -readability-identifier-length

CheckOptions:
bugprone-assert-side-effect.CheckFunctionCalls: true
performance-move-const-arg.CheckTriviallyCopyableMove: false
cppcoreguidelines-avoid-do-while.IgnoreMacros: true
246 changes: 218 additions & 28 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,27 +1,5 @@
# Aether Client C++ Guide

## Modes and Persistence

`aether` has three persistence behaviors:

- **Distillation** (`AE_DISTILLATION=On`): create every object from scratch,
even when persistent state already exists. Use it during development and as
preparation for production.
- **Filtration** (`AE_FILTRATION=On`): load an object when state exists and
create it when it does not. Filtration also enables the distillation code
paths at compile time. It is useful when applications should tolerate both
existing and missing state.
- **Production**: both `AE_DISTILLATION` and `AE_FILTRATION` are disabled or
undefined. Main persistent objects, such as `Aether`, adapters, clients, and
clouds, must already exist. Some argument-taking constructors are disabled;
objects must be loaded from the domain or copied from prefab objects.

Production is descriptive terminology, not a separate build option.

`FS_INIT` may provide generated or static persisted-state maps. Persistence is
not implied by mutation; use the established application save path when state
must survive shutdown.

## Project Model

`aether` is a C++20 static library for persistent state, asynchronous actions
Expand All @@ -43,6 +21,10 @@ persistent `Obj` types.
and implement the established `Load`/`Save` patterns.
- Use `ae::ObjPtr<T>` for strong references to persistent objects.
- Use `ae::Ptr<T>` for shared ownership of non-`Obj` objects.
- `ae::Ptr<T>` uses reachability counting on the reference graph to reclaim
cyclic references. Releasing a pointer may therefore be expensive; avoid
unnecessary copies, pass it by reference when possible, and move it when
transferring ownership.
- `ae::PtrView<T>` is a weak, nullable view. Lock/load it before retaining or
dereferencing the object.
- A valid `ObjPtr` may still refer to an unloaded object. Load it before use
Expand Down Expand Up @@ -178,6 +160,93 @@ ordinary errors.
- Use `SubApi<T>` and the existing API context/parser patterns for nested API
calls instead of inventing a separate packet format.

## Serialization

Serialization is provided by the `aether-miscpp` dependency. It is used both
to save and load persistent `Obj` state and to encode and decode API protocol
messages.

There are three ways to make a type serializable:

- Use the project's reflection support when the type is a straightforward
aggregate of serializable members.
- Provide a `seri::Serializer<Archive, Type>` specialization. Prefer this
approach because it keeps serialization logic separate from the model type.
- Add `Seri()` and `Deseri()` member functions when serialization intrinsically
belongs to the type or a member serializer is otherwise the best fit.

Implement a serializer against the general `seri::Archive` concept when the
representation is independent of the underlying archive. Specialize it for a
specific archive when the representation depends on that archive's storage or
wire format. The currently available concrete archive is
`seri::BinaryArchive<BinaryBuffer>`.

A serializer provides `Seri()` for saving and `Deseri()` for loading. Saving
receives `Meta<T const>` and loading receives `Meta<T>`; both return
`SeriResult`:

```cpp
namespace ae::seri {
template <Archive A>
struct Serializer<A, MyType> {
SeriResult Seri(A& archive, Meta<MyType const> meta) const {
return archive.Save(Meta{meta.value.member});
}

SeriResult Deseri(A& archive, Meta<MyType> meta) const {
return archive.Load(Meta{meta.value.member});
}
};
} // namespace ae::seri
```

`BinaryBuffer` exposes two pairs of `Read` and `Write` operations. The size
operation represents a container size, meaning a count of elements. The data
and size operation represents a payload together with its size, for one or
more elements. Different buffer implementations may use different physical
representations for the size and data, so serializers should use the buffer
operations rather than assuming a particular layout.

## Reflection

Reflection is provided by the `aether-miscpp` dependency. It describes the
members of a type so generic code can inspect or process them, including
serialization and other algorithms.

For regular members, declare the reflected members in the type with:

```cpp
AE_REFLECT_MEMBERS(a, b, c)
```

For explicit reflection entries, use `AE_REFLECT` and pass it reflection
entries. Use `AE_MMBR(member)` for one regular member or
`AE_MMBRS(first, second)` for multiple regular members. Use `AE_REF(member)`
when a reflected member is a reference. For example, use
`AE_REFLECT(AE_REF(member))` for a single explicit reference member. For base
classes, `AE_REFLECT` supports both `AE_REF_BASE(Base)` and `AE_BASE(Base)`:

- Use `AE_REF_BASE(Base)` to reflect a reference to `Base` as one member.
- Use `AE_BASE(Base)` to concatenate `Base`'s reflected members into the
derived type's reflection.

All explicit reflection helpers can be combined in one declaration:

```cpp
AE_REFLECT(AE_MMBR(member), AE_MMBRS(first, second), AE_REF(reference),
AE_REF_BASE(BaseAsMember), AE_BASE(BaseMembers));
```

Create a reflection object with `ae::make_reflection(obj)` and apply a
callable to all reflected members with `Apply()`:

```cpp
auto reflection = ae::make_reflection(obj);
reflection.Apply([](auto&&... members) {
// Process the reflected members.
});
```

## Streams

- Streams publish state and data through events; writes return actions.
Expand All @@ -204,6 +273,38 @@ configured through `aether/tele.h`.
- Register a module tag when tagged logging is needed.
- Use registered tags with `AE_TELE_<LEVEL>(kTag, ...)`.

### Format

`Format` is provided by the `aether-miscpp` dependency and can be used on its
own to build formatted strings or to provide a format string to a telemetry
log, for example:

```cpp
AE_TELE_DEBUG(kTag, "Format string {}", data);
```

- Use `{}` for replacement fields. Arguments are consumed from left to right;
for example, `Format("id={}, state={}", id, state)`. Formatting schemes can
be selected after a colon, such as `{:time}` for time values.
- To make a project type formattable, specialize `ae::Formatter<YourType>` and
implement `Format(YourType const&, FormatContext<TStream>&) const`. Write
output through `ctx.out()`, or delegate to existing formatters with
`Formatter<T>{}.Format(value, ctx)`. For a composed representation, use
`FormatTo(ctx.out(), FormatScheme{"value={}, count={}"}, value, count)`:

```cpp
namespace ae {
template <>
struct Formatter<MyType> {
template <typename TStream>
void Format(MyType const& value, FormatContext<TStream>& ctx) const {
FormatTo(ctx.out(), FormatScheme{"name={}, count={}"}, value.name,
value.count);
}
};
} // namespace ae
```

## C++ Coding Rules

- Follow the Google C++ Style Guide.
Expand Down Expand Up @@ -271,13 +372,102 @@ explicitly requested to prove specific behavior.

## Build and Configuration

Use the regular root CMake project, enable the required `AE_BUILD_*` options,
build the requested targets, and run their tests from the same build directory.
Run clang-tidy on changed C++ files using that build's matching
`compile_commands.json`; regenerate it when configuration flags change.
Use the regular root CMake project. Keep separate build directories for
different compilers, build types, sanitizers, persistence modes, and user
configuration headers. A configured build directory retains its CMake options,
so inspect or reconfigure it before relying on its settings.

Use a separate build directory such as `<build_dir>` for each compiler,
platform, build type, sanitizer, persistence mode, or user configuration.
Build and test it with:

```bash
cmake --build <build_dir> --parallel
ctest --test-dir <build_dir> --output-on-failure
```

A successful CMake configure is not build or test validation.

### Compile-Time Configuration

`aether/config.h` provides the built-in configuration defaults. `USER_CONFIG`
is optional; when defined, `aether/config.h` includes the selected header before
applying its remaining `#ifndef` defaults. Therefore a user configuration header
overrides the defaults by defining the relevant `AE_*` macros.

No user configuration is selected when `USER_CONFIG` is empty. This is the
project's default behavior and uses the values from `aether/config.h`.

Select one of the predefined configurations with a path relative to the source
tree, for example:

```bash
cmake -S . -B build-hydrogen \
-DUSER_CONFIG=config/user_config_hydrogen.h \
-DAE_BUILD_TESTS=ON
```

Predefined configurations are located in `config/`. Inspect the selected
configuration before changing code that depends on compile-time feature or
cryptography settings.

Custom configuration headers may also be supplied through CMake:

```bash
cmake -S . -B build-custom \
-DUSER_CONFIG=/absolute/path/to/my_aether_config.h
```

Configuration changes require a separate build directory or a CMake reconfigure,
and can change available source features and required platform dependencies.

`USER_CONFIG` is a compile-time configuration header, not persisted state.
`FS_INIT` optionally supplies generated or static saved-state data:

```bash
cmake -S . -B build-with-state \
-DUSER_CONFIG=config/user_config_hydrogen.h \
-DFS_INIT=/absolute/path/to/generated_state.h
```

### Persistence Build Modes

`AE_DISTILLATION` and `AE_FILTRATION` are independent CMake options:

- `AE_DISTILLATION=ON` enables creation of objects from scratch, even when
persisted state exists.
- `AE_FILTRATION=ON` enables loading existing state and creating missing
objects. In `aether/config.h`, filtration also defines `AE_DISTILLATION=1`
so code requiring distillation support is compiled.
- With both options disabled, production behavior is used: required persistent
objects must already exist and be loaded from the domain or copied from
prefab objects.

Examples:

```bash
# Development: always create state
cmake -S . -B build-distillation -DAE_DISTILLATION=ON -DAE_FILTRATION=OFF

# Hybrid operation: load existing state or create it
cmake -S . -B build-filtration -DAE_DISTILLATION=OFF -DAE_FILTRATION=ON

# Production behavior: neither mode enabled
cmake -S . -B build-production -DAE_DISTILLATION=OFF -DAE_FILTRATION=OFF
```

`FS_INIT` may provide generated or static persisted-state maps. Persistence is
not implied by mutation; use the established application save path when state
must survive shutdown.

### Formatting and Static Checks

`USER_CONFIG` selects the compile-time user-configuration header. The prescribed
operational default is `./config/user_config_hydrogen.h`.
Follow the repository's Google C++ style and warning policy. Keep includes
minimal and preserve intentional public umbrella includes with IWYU annotations.
Use the configured build's `compile_commands.json` for changed-file clang-tidy
checks. Regenerate the compilation database when compiler, CMake options,
platform, or user configuration changes. Apply formatting consistently with
the repository's existing `.clang-format` policy before submitting changes.

For ESP-IDF, use the covered project at
`projects/xtensa_lx6/vscode/aether-client-cpp`. Select the appropriate ESP32
Expand Down
9 changes: 5 additions & 4 deletions aether/ae_actions/ping.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ Ping::Ping(AeContext const& ae_context,
next_ping_hint_{next_ping_hint},
rx_window_{rx_window},
timeout_{timeout},
server_id_{cloud_server_connection_->server()->server_id} {
server_id_{cloud_server_connection_->server_id()} {
AE_TELE_INFO(
kPing,
"Ping action created to server id: {}, interval: {:%S}s, rx_window: "
Expand Down Expand Up @@ -122,9 +122,10 @@ void Ping::Start(TimePoint current_time) {
[this, req_id]() { PingResponseTimeout(req_id); },
current_time + timeout_);
if (state_ == RequestState::kPending && !timeout_sub_) {
AE_TELE_ERROR(kPingTimeoutError,
"Ping timeout task allocation failed server id {} request {}",
server_id_, req_id);
AE_TELE_ERROR(
kPingTimeoutError,
"Ping timeout task allocation failed server id {} request {}",
server_id_, req_id);
state_ = RequestState::kFinished;
ResetRequestSubscriptions();
result_event_.Emit(PingResult{Error{5}});
Expand Down
15 changes: 8 additions & 7 deletions aether/aether.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,13 @@

#include <utility>

#include "aether/obj/obj_ptr.h"
#include "aether/client.h"
#include "aether/server.h"
#include "aether/obj/obj_ptr.h"
#include "aether/registration_cloud.h"
#include "aether/server.h"

#include "aether/work_cloud.h"
#include "aether/registration/registration.h"
#include "aether/work_cloud.h"

#include "aether/aether_tele.h"

Expand Down Expand Up @@ -75,9 +75,8 @@ Client::ptr Aether::CreateClient(ClientConfig const& config,

auto client_cloud = WorkCloud::ptr::Create(domain, config.uid);
[[maybe_unused]] auto res = // ~(^.^)~
client_cloud.WithLoaded([&](auto const& cloud) {
cloud->SetServers(std::move(servers));
}) && // ~(^.^)~
client_cloud.WithLoaded(
[&](auto const& cloud) { cloud->SetServers(servers); }) && // ~(^.^)~
client.WithLoaded([&](auto const& client) {
client->SetConfig(client_id, config.parent_uid, config.uid,
config.ephemeral_uid, config.master_key,
Expand Down Expand Up @@ -135,7 +134,9 @@ Client::ptr Aether::FindClient(std::string const& client_id) {

void Aether::StoreClient(Client::ptr client) {
assert(client.is_valid() && "Client is invalid");
clients_[client.Load()->id()] = std::move(client);
auto const loaded_client = client.Load();
assert(loaded_client && "Client failed to load");
clients_[loaded_client->id()] = std::move(client);
}

SelectClientAction* Aether::FindSelectClientAction(
Expand Down
5 changes: 3 additions & 2 deletions aether/client.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,9 @@ void Client::SetConfig(std::string client_id, Uid parent_uid, Uid uid,
master_key_ = std::move(master_key);
cloud_ = std::move(cloud);

for (auto& s : cloud_->servers()) {
server_keys_.emplace(s->server_id, ServerKeys{s->server_id, master_key_});
for (auto const& server_entry : cloud_->servers()) {
auto const server_id = server_entry.first;
server_keys_.emplace(server_id, ServerKeys{server_id, master_key_});
}

connectivity_policy_ = ClientConnectivityPolicy::ptr::Create(
Expand Down
Loading
Loading