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.
sc::string_constant
uses a user-defined literal to make it easy to define a
string:
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.
using HelloWorldType = decltype("Hello, World!"_sc);
constexpr HelloWorldType helloWorld{};
static_assert(helloWorld == "Hello, World!"_sc);
Two strings can be joined together using the +
operator:
static_assert("Hi "_sc + "Emily!"_sc == "Hi Emily!"_sc);
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.
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.
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.
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.
// 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:
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.