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.
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++ doesn’t 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:
if (SyncCall(handler, &LongAsyncComputation, &s, &i) == ContinuationStatus::INTERRUPTED) { return ContinuationStatus::INTERRUPTED; } FXL_LOG(INFO) << "LongAsyncComputation returned: " << s << " " << i; // Possibly make further async calls.
Instead of:
LongAsyncComputation([] (auto s, auto i) { FXL_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:
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.
callback
library with fit::callback
, which is a single util within the fit library under //zircon/system/ulib/fit
).Containers:
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.Facilities that help manage lifetime:
Various utils related to callbacks:
A
into std::vector<A>
A
into the first result availableS
into status OK if all of them succeeded, or the first error status if any of them failedTypes 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.:
namespace cloud_provider { using CloudProvider = fuchsia::ledger::cloud::CloudProvider; }
See example header.
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:
storage->GetCommit( parent_id, callback::Capture(callback::SetWhenCalled(&called), &status, &base)); RunLoopUntilIdle(); EXPECT_TRUE(called); EXPECT_EQ(storage::Status::OK, status);
The tests use the main function defined in //src/lib/fxl/test/run_all_unittests.cc. This provides additional features:
See Testing for documentation on different types of tests and how to run them.