add Chapter18

This commit is contained in:
Amar Mahmutbegovic
2025-02-06 00:19:59 +01:00
parent 9cc9cc7d73
commit 8634accda5
1533 changed files with 1092521 additions and 0 deletions

View File

@@ -0,0 +1,179 @@
# string_constant
An `sc::string_constant` is a compile-time string in the same way
that `std::integral_constant` is a compile-time integral value. When firmware
needs to emit a log message, an integral ID is emitted instead of the string
data. This conserves both runtime and storage resources. When decoding the log
messages, a catalog file is needed to map the IDs back to their string values.
There are two challenges with such a system. The first is assigning a unique ID
to each string value and the second is how to support rich string operations
like format and concatenation.
The `sc::string_constant` library solves both of these challenges by
representing the value of the string in its type. The `sc::string_constant`'s
string template struct is parameterized by a list of chars representing the
string value as well as a tuple of zero or more dynamic arguments to be
formatted by a log decoder.
The details of this implementation are provided in the Theory of Operation
section.
[![Embedded Logging Case Study: From C to Shining C++ - Luke Valenty -CppNow 2022](https://img.youtube.com/vi/Dt0vx-7e_B0/0.jpg)](https://www.youtube.com/watch?v=Dt0vx-7e_B0)
## Usage
### `_sc` user defined literal
`sc::string_constant` uses a user-defined literal to make it easy to define a
string:
```c++
constexpr auto hello_world = "Hello, World!"_sc;
```
Note how the auto keyword is used for the string type. This is necessary because
the string's type changes depending on its value. It would be redundant and
burdensome to specify the exact type.
Also note the `constexpr` keyword is used. `sc::string_constant`s are fully
`constexpr` and nearly all operations are performed at compile-time even without
the `constexpr` keyword.
Because the string value is represented by its type, strings can be assigned as
type aliases or passed in as template parameters.
```c++
using HelloWorldType = decltype("Hello, World!"_sc);
constexpr HelloWorldType helloWorld{};
static_assert(helloWorld == "Hello, World!"_sc);
```
### Concatenation
Two strings can be joined together using the `+` operator:
```c++
static_assert("Hi "_sc + "Emily!"_sc == "Hi Emily!"_sc);
```
### Format
`sc::string_constant`s can be formatted using a subset of python
or `fmt::format`
format specifiers. Formatting behavior is different depending on whether the
arguments are compile-time values or dynamic values. Compile-time values will be
formatted at compile time while dynamic values will only be formatted by tools
outside of firmware like a log decoder.
```c++
constexpr auto my_age = 6;
constexpr auto day_to_go = day_of_week::SATURDAY;
auto my_name = "Olivia"_sc;
using BirthdayParty = party<party_type::Birthday, day_of_week::TUESDAY>;
// use int_<value> to format an integer known at compile time
static_assert(
format("I am {} years old."_sc, sc::int_<my_age>) ==
"I am 6 years old."_sc);
// use enum_<value> to format an enum value known at compile time
static_assert(
format("Let's go on {}."_sc, sc::enum_<day_to_go>) ==
"Let's go on SATURDAY."_sc);
// strings can be used as format arguments as well
static_assert(
format("My name is {}."_sc, my_name) ==
"My name is Olivia."_sc);
// use type_<type> to get the string of a type which can then be used as a format argument
static_assert(
type_<BirthdayParty> ==
"party<party_type::Birthday, day_of_week::TUESDAY>"_sc);
// multiple arguments can be formatted at once
static_assert(
format("My name is {} and I am {} years old."_sc, my_name, my_age) ==
"My name is Olivia and I am 6 years old."_sc);
// runtime arguments not known at compile-time can also be formatted. the dynamic values can be
// accessed at runtime and emitted along with the string_constant id.
void read_memory(std::uint32_t addr) {
auto const my_message = format("Reading memory at {:08x}"_sc, addr);
}
// both runtime and compile time arguments can be used in a single format operation
template<typename RegType>
void readReg() {
RegType reg{};
auto const reg_value = apply(read(reg.raw()));
auto const my_message = format("Register {} = {}"_sc, type_<RegType>, reg_value);
}
```
Note that values known at compile time are passed into template variables as
template parameters. This allows the string.format method to access constexpr
versions of the values and format them into the string.
If you format integers or enums without using the template variables, then the
formatting happens outside of firmware by the log decoder collateral.
## Theory of Operation
Compile-time string literals are created using user-defined string template
literals. These are not yet part of a C++ standard but are supported by GCC,
Clang, and MSVC. They allow for easy conversion of a string literal to template
parameters.
```c++
template<class T, T... chars>
constexpr auto operator""_sc() {
return sc::string_constant<T, chars...>{};
}
```
We can see clearly that the string value is encoded in the type.
```c++
// create a compile-time string. note that it does not need to be constexpr or even const because the value is encoded in the type.
auto hello_value = "Hello!"_sc;
using HelloType = decltype(hello_value);
// the string's value is represented by its type
using ExpectedType = sc::string_constant<char, 'H', 'e', 'l', 'l', 'o', '!'>;
ExpectedType expected_value{};
// same type
static_assert(std::is_same_v<HelloType, ExpectedType>);
// same value
static_assert(hello_value == expected_value);
```
Encoding the string value in the type is necessary for string_constant support.
Type information is used when linking object files together. This means the
string values can be present in object files to be extracted during the build
process. The other part is assigning a unique ID to each string. This can be
done with the declaration of a template function with external linkage like
this:
```c++
template<typename StringType>
extern std::uint32_t get_string_id(StringType);
```
When a string needs to be emitted as part of a log message, the `get_string_id()`
function is used to translate the string type to its ID. During compilation, an
intermediate object file is generated missing a definition for each
instantiation of the `get_string_id()` function. This information can be extracted
using the 'nm' tool and the original string values recovered. A
new `strings.cpp` file can be generated that implements all the template
instantiations returning a unique ID for each string. When the original object
binary is linked to `strings.o`, the calls to `get_string_id()` can be replaced
with the actual int value.

View File

@@ -0,0 +1,103 @@
#pragma once
#include <sc/fwd.hpp>
#include <sc/lazy_string_format.hpp>
#include <sc/string_constant.hpp>
#include <stdx/compiler.hpp>
#include <stdx/ct_conversions.hpp>
#include <stdx/tuple.hpp>
#include <stdx/tuple_algorithms.hpp>
#include <fmt/compile.h>
#include <fmt/format.h>
#include <algorithm>
#include <array>
#include <concepts>
#include <cstddef>
#include <iterator>
#include <type_traits>
#include <utility>
namespace sc {
namespace detail {
template <typename T>
concept compile_time_field =
std::same_as<typename T::value_type,
std::remove_cvref_t<decltype(T::value)>>;
template <compile_time_field T> [[nodiscard]] CONSTEVAL auto field_value(T) {
if constexpr (std::is_enum_v<typename T::value_type>) {
return stdx::enum_as_string<T::value>();
} else {
return T::value;
}
}
template <typename T>
[[nodiscard]] CONSTEVAL auto field_value(sc::type_name<T>) {
return stdx::type_as_string<T>();
}
template <typename Fmt, typename Arg> constexpr auto format1(Fmt, Arg arg) {
constexpr auto str = [&] {
constexpr auto fmtstr = FMT_COMPILE(Fmt::value);
constexpr auto sz = fmt::formatted_size(fmtstr, field_value(arg));
std::array<char, sz> buf{};
fmt::format_to(std::begin(buf), fmtstr, field_value(arg));
return buf;
}();
return [&]<std::size_t... Is>(std::index_sequence<Is...>) {
return string_constant<char, str[Is]...>{};
}(std::make_index_sequence<std::size(str)>{});
}
template <typename Fmt> constexpr auto split_format_spec() {
constexpr Fmt fmt{};
constexpr auto spec_start = std::adjacent_find(
std::begin(fmt), std::end(fmt),
[](auto c1, auto c2) { return c1 == '{' and c2 != '{'; });
if constexpr (spec_start == std::end(fmt)) {
return std::pair{fmt, ""_sc};
} else {
constexpr auto spec_end = std::find_if(spec_start, std::end(fmt),
[](auto c) { return c == '}'; });
constexpr auto len =
static_cast<int>(std::distance(std::begin(fmt), spec_end) + 1);
return std::pair{fmt.substr(int_<0>, int_<len>), fmt.substr(int_<len>)};
}
}
template <typename Str, typename Fmt, typename RuntimeTuple, typename Arg>
constexpr auto process_arg(stdx::tuple<Str, Fmt, RuntimeTuple> t, Arg arg) {
using namespace stdx::literals;
constexpr auto p = split_format_spec<Fmt>();
if constexpr (requires { field_value(arg); }) {
return stdx::make_tuple(t[0_idx] + format1(p.first, arg), p.second,
t[2_idx]);
} else if constexpr (requires { arg.args; }) {
return stdx::make_tuple(t[0_idx] + format1(p.first, arg.str), p.second,
stdx::tuple_cat(t[2_idx], arg.args));
} else {
return stdx::make_tuple(
t[0_idx] + p.first, p.second,
stdx::tuple_cat(t[2_idx], stdx::make_tuple(arg)));
}
}
} // namespace detail
template <typename Fmt, typename... Args>
constexpr auto format(Fmt, Args... args) {
using namespace stdx::literals;
auto t = stdx::make_tuple(args...);
auto r =
t.fold_left(stdx::make_tuple(""_sc, Fmt{}, stdx::tuple{}),
[](auto x, auto y) { return detail::process_arg(x, y); });
if constexpr (r[2_idx].size() == 0) {
return r[0_idx] + r[1_idx];
} else {
return lazy_string_format{r[0_idx] + r[1_idx], r[2_idx]};
}
}
} // namespace sc

View File

@@ -0,0 +1,46 @@
#pragma once
#include <stdx/compiler.hpp>
#include <stdx/ct_string.hpp>
#include <concepts>
#include <type_traits>
namespace sc {
template <std::signed_integral auto value>
constexpr static std::integral_constant<decltype(value), value> int_{};
template <std::unsigned_integral auto value>
constexpr static std::integral_constant<decltype(value), value> uint_{};
template <bool value>
constexpr static std::integral_constant<bool, value> bool_{};
template <char value>
constexpr static std::integral_constant<char, value> char_{};
template <auto enumValue>
requires std::is_enum_v<decltype(enumValue)>
constexpr static std::integral_constant<decltype(enumValue), enumValue> enum_{};
template <typename T> struct type_name {
constexpr explicit type_name(T) {}
constexpr type_name() = default;
};
template <typename T> constexpr static type_name<T> type_{};
template <typename CharT, CharT... chars> struct string_constant;
inline namespace literals {
template <stdx::ct_string S> CONSTEVAL auto operator""_sc() {
return stdx::ct_string_to_type<S, string_constant>();
}
} // namespace literals
template <typename T>
concept sc_like = requires(T const &t) {
t.apply([]<typename StringType>(StringType, auto const &...) {});
};
} // namespace sc
using sc::literals::operator""_sc;

View File

@@ -0,0 +1,56 @@
#pragma once
#include <sc/fwd.hpp>
#include <stdx/tuple_algorithms.hpp>
namespace sc {
template <typename StringConstant, typename ArgTuple>
struct lazy_string_format {
constexpr static StringConstant str{};
ArgTuple args{};
constexpr lazy_string_format() = default;
constexpr lazy_string_format(StringConstant, ArgTuple newArgs)
: args{newArgs} {}
template <typename F> constexpr auto apply(F &&f) const {
return args.apply(
[&](auto const &...as) { return std::forward<F>(f)(str, as...); });
}
};
template <class CharT, CharT... charsLhs, typename ArgsTupleLhs,
CharT... charsRhs, typename ArgsTupleRhs>
[[nodiscard]] constexpr auto operator==(
lazy_string_format<string_constant<CharT, charsLhs...>, ArgsTupleLhs> lhs,
lazy_string_format<string_constant<CharT, charsRhs...>, ArgsTupleRhs>
rhs) noexcept -> bool {
return (lhs.str == rhs.str) && (lhs.args == rhs.args);
}
template <typename StringConstantLhs, typename TupleArgsLhs,
typename StringConstantRhs, typename TupleArgsRhs>
[[nodiscard]] constexpr auto
operator+(lazy_string_format<StringConstantLhs, TupleArgsLhs> lhs,
lazy_string_format<StringConstantRhs, TupleArgsRhs> rhs) noexcept {
return lazy_string_format{lhs.str + rhs.str,
stdx::tuple_cat(lhs.args, rhs.args)};
}
template <typename StringConstantLhs, typename TupleArgsLhs, typename CharT,
CharT... chars>
[[nodiscard]] constexpr auto
operator+(lazy_string_format<StringConstantLhs, TupleArgsLhs> lhs,
string_constant<CharT, chars...> rhs) noexcept {
return lazy_string_format{lhs.str + rhs, lhs.args};
}
template <typename CharT, CharT... chars, typename StringConstantRhs,
typename TupleArgsRhs>
[[nodiscard]] constexpr auto
operator+(string_constant<CharT, chars...> lhs,
lazy_string_format<StringConstantRhs, TupleArgsRhs> rhs) noexcept {
return lazy_string_format{lhs + rhs.str, rhs.args};
}
} // namespace sc

View File

@@ -0,0 +1,108 @@
#pragma once
#include <sc/fwd.hpp>
#include <array>
#include <iterator>
#include <limits>
#include <string_view>
#include <utility>
namespace sc {
template <typename CharT, CharT... chars> struct string_constant {
using value_type = std::basic_string_view<CharT>;
private:
constexpr static std::array<CharT, sizeof...(chars)> storage{chars...};
using size_type = int;
using const_iterator = typename value_type::const_iterator;
constexpr static size_type npos = std::numeric_limits<size_type>::max();
public:
constexpr static value_type value{storage.data(), sizeof...(chars)};
constexpr static auto begin() noexcept { return std::cbegin(storage); }
constexpr static auto end() noexcept { return std::cend(storage); }
[[nodiscard]] constexpr static auto size() noexcept {
return std::size(storage);
}
template <size_type pos = 0, size_type count = npos>
[[nodiscard]] constexpr static auto
substr(std::integral_constant<size_type, pos>,
std::integral_constant<size_type, count> = {}) {
constexpr size_type sz = count == npos ? size() - pos : count;
return [&]<size_type... Is>(std::integer_sequence<size_type, Is...>) {
return string_constant<CharT, storage[pos + Is]...>{};
}(std::make_integer_sequence<size_type, sz>{});
}
template <typename F> constexpr auto apply(F &&f) const {
return std::forward<F>(f)(*this);
}
};
template <class CharT, CharT... charsLhs, CharT... charsRhs>
[[nodiscard]] constexpr auto
operator==(string_constant<CharT, charsLhs...>,
string_constant<CharT, charsRhs...>) noexcept -> bool {
return false;
}
template <class CharT, CharT... chars>
[[nodiscard]] constexpr auto
operator==(string_constant<CharT, chars...>,
string_constant<CharT, chars...>) noexcept -> bool {
return true;
}
#if __cpp_lib_three_way_comparison < 201907L
template <class CharT, CharT... charsLhs, CharT... charsRhs>
[[nodiscard]] constexpr auto
operator!=(string_constant<CharT, charsLhs...> lhs,
string_constant<CharT, charsRhs...> rhs) noexcept -> bool {
return not(lhs == rhs);
}
template <class CharT, CharT... charsLhs, CharT... charsRhs>
[[nodiscard]] constexpr auto
operator<(string_constant<CharT, charsLhs...> lhs,
string_constant<CharT, charsRhs...> rhs) noexcept -> bool {
return lhs.value < rhs.value;
}
template <class CharT, CharT... charsLhs, CharT... charsRhs>
[[nodiscard]] constexpr auto
operator>(string_constant<CharT, charsLhs...> lhs,
string_constant<CharT, charsRhs...> rhs) noexcept -> bool {
return lhs.value > rhs.value;
}
template <class CharT, CharT... charsLhs, CharT... charsRhs>
[[nodiscard]] constexpr auto
operator<=(string_constant<CharT, charsLhs...> lhs,
string_constant<CharT, charsRhs...> rhs) noexcept -> bool {
return lhs.value <= rhs.value;
}
template <class CharT, CharT... charsLhs, CharT... charsRhs>
[[nodiscard]] constexpr auto
operator>=(string_constant<CharT, charsLhs...> lhs,
string_constant<CharT, charsRhs...> rhs) noexcept -> bool {
return lhs.value >= rhs.value;
}
#else
template <class CharT, CharT... charsLhs, CharT... charsRhs>
[[nodiscard]] constexpr auto
operator<=>(string_constant<CharT, charsLhs...> lhs,
string_constant<CharT, charsRhs...> rhs) noexcept {
return lhs.value <=> rhs.value;
}
#endif
template <class CharT, CharT... charsLhs, CharT... charsRhs>
[[nodiscard]] constexpr auto
operator+(string_constant<CharT, charsLhs...>,
string_constant<CharT, charsRhs...>) noexcept
-> string_constant<CharT, charsLhs..., charsRhs...> {
return {};
}
} // namespace sc