blob: cc05a235de12b855e94bfc87bb2d3b972147abd6 [file] [log] [blame] [view]
# C++ in Ledger
Overview of the idioms, patterns and libraries specific to Ledger C++ codebase.
The intention is to not duplicate information already present in the individual
header files, but rather to provide a guided tour of the most interesting header
files with TL:DR of when to use each.
[TOC]
## coroutines
The Ledger codebase (and Fuchsia in general) is highly asynchronous, making the code
prone to the callback hell highly nested and difficult to follow code that
executes async operations in response to async operations.
The standard solution to this in languages such as Dart or Python is
async/await, allowing to concisely express the fact that a step of the function
is asynchronous without the additional level of nesting. However, C++ doesnt
have a similar canonical solution.
As some parts of the Ledger codebase need to perform particularly complex chains of
async operations, we developed a custom solution that approximates async/await
using coroutines. This allows us to write code like this:
```cpp
if (SyncCall(handler, &LongAsyncComputation, &s, &i) ==
ContinuationStatus::INTERRUPTED) {
return ContinuationStatus::INTERRUPTED;
}
LEDGER_LOG(INFO) << "LongAsyncComputation returned: " << s << " " << i;
// Possibly make further async calls.
```
Instead of:
```cpp
LongAsyncComputation([] (auto s, auto i) {
LEDGER_LOG(INFO) << "LongAsyncComputation returned: " << s << " " << i;
// Possibly make further async calls.
});
```
Coroutines make it possible to implement SyncCall by allowing us to pause the
current execution context of a thread (freeing the thread to do other things)
and resume it later. The implementation of coroutines we use is custom
(Fuchsia-specific) and lives under //src/ledger/lib/coroutine/coroutine.h .
When working with coroutines:
* manage them using coroutine_manager.h this util helps to make sure that
coroutines that refer to this object are shut down when the object goes
away;
* use coroutine::SyncCall to await for results of an async call. You should
not need to manually yield or resume coroutines.
* when SyncCall returns ContinuationStatus::INTERRUPTED, return immediately.
## callback library
Ledger makes extensive use of the [callback] library. It is a collection of
utilities we found useful for developing Ledger they were originally written
by the Ledger team but now live outside of //src/ledger because other teams
wanted to use them.
***note
Note do not confuse the `callback` library with `fit::callback`, which is
a single util within the [fit] library under `//zircon/system/ulib/fit`).
***
**Containers**:
* [auto_cleanable.h] provides two container types which automatically manage
deleting elements from them: AutoCleanableMap and AutoCleanableSet.
Automatic here means that all elements stored in the containers are given
an OnDiscardable callback (via a
`void SetOnDiscardable(fit::closure on_discardable)` method that they must
implement) when this callback is called, they are deleted from the parent
container. Useful for building hierarchical collections of objects.
* TODO: add a file-level comment in this header
* [managed_container.h] TODO: what is it for?
Facilities that help manage **lifetime**:
* [scoped_task_runner.h] and [scoped_callback.h] useful when an object posts
tasks that refer to the object itself, but could end up being executed after
the object itself is deleted.
* [scoped_callback.h] allows to wrap any callback in a lambda that, when
called, executes the initial callback only if a given witness is still
true by using a weak pointer to our object, we can ensure that stale
callbacks are not executed.
* TODO: add a file-level comment in this header
* [scoped_task_runner.h] manages this automatically its a wrapper over
a regular task runner than wraps each posted task into a scoped callback
with a weak pointer to the scoped task runner itself.
* [destruction_sentinel.h] useful when implementing an object method that
may directly or indirectly delete the object itself within the method body
(e.g. by triggering the cleanup of the object from the container in which it
is stored)
Various utils related to callbacks:
* [operation_serializer.h] this is serialization in the sense of executing
one after another, not in the sense of producing a binary representation.
The util takes async operations and executes each only if all the previously
inserted operations have already completed
* [trace_callback.h] wraps a callback in tracing instrumentation
* [waiter.h] utilities that collect results of multiple asynchronous
operations into a single asynchronous callback that is called when all
results are ready. This includes:
* Waiter collects results of multiple callbacks producing type `A` into
`std::vector<A>`
* AnyWaiter collects results of multiple callbacks producing type `A`
into the first result available
* StatusWaiter collects results of multiple callbacks producing only
completion status `S` into status OK if all of them succeeded, or the
first error status if any of them failed
* [ensure_called.h] ensures that a function is called whatever happens with
default arguments, but can also be called manually with specific arguments,
and its result can be retrieved. This is similar but more general than
[defer.h], which is never called manually.
* TODO: add a file-level comment in this header
## FIDL
Types generate from FIDL files are a bit unwieldy (e.g.
`fuchsia::ledger::cloud::CloudProvider`). We use header files that define
aliases for them, e.g.:
```cpp
namespace cloud_provider {
using CloudProvider = fuchsia::ledger::cloud::CloudProvider;
}
```
See [example header](/src/ledger/bin/fidl/include/types.h).
## testing
Most tests are based on [test_loop_fixture.h], which abstracts the time and
provides a useful RunLoopUntilIdle()” method.
Together with the callback library helpers `Capture` and `SetWhenCalled` (see
[capture.h]) and [set_when_called.h]), this provides a handy pattern for
concisely verifying results of an async call:
```cpp
storage->GetCommit(
parent_id,
Capture(SetWhenCalled(&called), &status, &base));
RunLoopUntilIdle();
EXPECT_TRUE(called);
EXPECT_EQ(storage::Status::OK, status);
```
The tests use the main function defined in [run_all_unittests_main.cc]. This
provides additional features:
* control log settings via `--verbose`,
* control `TestLoop`'s random seed via `--test_seed_loop` to easily reproduce a
random-related flakiness (the random seed is logged whenever a test is
started)
See [Testing](testing.md) for documentation on different types of tests and how
to run them.
[auto_cleanable.h]: /src/ledger/lib/callback/auto_cleanable.h
[callback]: /src/ledger/lib/callback
[capture.h]: /src/ledger/lib/callback/capture.h
[defer.h]: /zircon/system/ulib/fit/include/lib/fit/defer.h
[destruction_sentinel.h]: /src/ledger/lib/callback/destruction_sentinel.h
[ensure_called.h]: /src/ledger/lib/callback/ensure_called.h
[fit]: /zircon/system/ulib/fit
[managed_container.h]: /src/ledger/lib/callback/managed_container.h
[operation_serializer.h]: /src/ledger/lib/callback/operation_serializer.h
[run_all_unittests_main.cc]: /src/ledger/bin/testing/run_all_unittests_main.cc
[scoped_callback.h]: /src/ledger/lib/callback/scoped_callback.h
[scoped_task_runner.h]: /src/ledger/lib/callback/scoped_task_runner.h
[scoped_task_runner.h]: /src/ledger/lib/callback/scoped_task_runner.h
[set_when_called.h]: /src/ledger/lib/callback/set_when_called.h
[test_loop_fixture.h]: /src/ledger/lib/loop_fixture/test_loop_fixture.h
[trace_callback.h]: /src/ledger/lib/callback/trace_callback.h
[waiter.h]: /src/ledger/lib/callback/waiter.h