diff --git a/.github/workflows/asciidoctor-ghpages.yml b/.github/workflows/asciidoctor-ghpages.yml index 9f347fe9..45cc5a2a 100644 --- a/.github/workflows/asciidoctor-ghpages.yml +++ b/.github/workflows/asciidoctor-ghpages.yml @@ -42,7 +42,7 @@ jobs: - name: Install Mermaid run: | - npm install -g @mermaid-js/mermaid-cli@11.4.2 + npm install -g @mermaid-js/mermaid-cli@11.12.0 npx puppeteer browsers install chrome-headless-shell - name: Install asciidoctor diff --git a/docs/implementation_details.adoc b/docs/implementation_details.adoc deleted file mode 100644 index 483ced7b..00000000 --- a/docs/implementation_details.adoc +++ /dev/null @@ -1,157 +0,0 @@ - -== Implementation Details - -This section details some of the internal implementation details to assist contributors. -The details here are not required to use the `cib` library. - -=== Run Length Encoded Message Indices - -To switch to using the RLE indices is as simple as converting your `msg::indexed_service` to a -`msg::rle_indexed_service`. - -The initial building of the mapping indices proceeds the same as -the normal ones, where a series of entries in an index is generated -and the callback that match are encoded into a `stdx::bitset`. - -However, once this initial representation is built, we then take this and -perform additional work (at compile time) to encode the bitsets as RLE -data, and store in the index just an offset into the blob of RLE data -rather than the bitset itself. - -This is good for message maps that contain a large number of handlers as -we trade off storage space for some decoding overhead. - -Once encoded, the normal operation of the lookup process at run time -proceeds and a set of candidate matches is collected, these are then -_intersected_ from the RLE data and the final set of callbacks invoked -without needing to materialise any of the underlying bitsets. - -==== RLE Data Encoding - -There are several options for encoding the bitset into an RLE pattern, many of which will result -in smaller size, but a lot of bit-shifting to extract data. We have chosen to trade off encoded -size for faster decoding, as it is likely the handling of the RLE data and index lookup will be -in the critical path for system state changes. - -The encoding chosen is simply the number of consecutive bits of `0`​s or `1`​s. - -Specifics: - -- The encoding runs from the least significant bit to most significant bit -- The number of consecutive bits is stored as a `std::byte` and ranges `0...255` -- The first byte of the encoding counts the number of `0` bits -- If there are more than 255 consecutive identical bits, they can only be encoded in - blocks of 255, and an additional 0 is needed to indicate zero opposite bits are needed. - -[ditaa, format="svg", scale=1.5] ----- - Bitset RLE Data -/-------------+ +---+ -| 0b0000`0000 |--->| 8 | -+-------------/ +---+ - -/-------------+ +---+---+ -| 0b1111`1111 |--->| 0 | 8 | -+-------------/ +---+---+ - -/-------------+ +---+---+---+ -| 0b0000`0001 |--->| 0 | 1 | 7 | -+-------------/ +---+---+---+ - -/-------------+ +---+---+---+---+ -| 0b1000`0011 |--->| 0 | 2 | 5 | 1 | -+-------------/ +---+---+---+---+ - -/-------------+ +---+---+---+---+ -| 0b1100`1110 |--->| 1 | 3 | 2 | 2 | -+-------------/ +---+---+---+---+ - - -/------------------------------+ +---+---+-----+---+-----+---+-----+---+-----+ -| 1000 `0`s and one `1` in LSB |--->| 0 | 1 | 255 | 0 | 255 | 0 | 255 | 0 | 235 | -+------------------------------/ +---+---+-----+---+-----+---+-----+---+-----+ ----- - -The `msg::rle_indexed_builder` will go through a process to take the indices and -their bitset data and build a single blob of RLE encoded data for all indices, stored in -an instance of a `msg::detail::rle_storage`. It also generates a set of -`msg::detail::rle_index` entries for each of the index entries that maps the original bitmap -to a location in the shared storage blob. - -The `rle_storage` object contains a simple array of all RLE data bytes. The `rle_index` -contains a simple offset into that array. We compute the smallest size that can contain the -offset to avoid wasted storage and use that. - -NOTE: The specific `rle_storage` and `rle_index`​s are locked together using a unique type -so that the `rle_index` can not be used with the wrong `rle_storage` object. - -When building the shared blob, the encoder will attempt to reduce the storage size by finding -and reusing repeated patterns in the RLE data. - -The final `msg::indexed_handler` contains an instance of the `msg::rle_indices` which contains -both the storage and the maps referring to all the `rle_index` objects. - -This means that the final compile time data generated consists of: - -- The Message Map lookups as per the normal implementation, however they store a simple offset - rather than a bitset. -- The blob of all RLE bitset data for all indices in the message handling map - -==== Runtime Handling - -The `msg::indexed_handler` implementation will delegate the mapping call for an incoming -message down to the `msg::rle_indices` implementation. It will further call into it's -storage indices and match to the set of `rle_index` values for each mapping index. - -This set of `rle_index` values (which are just offsets) are then converted to instances of -a `msg::detail::rle_decoder` by the `rle_storage`. This converts the offset into a -pointer to the sequence of `std::byte`​s for the RLE encoding. - -All the collected `rle_decoders` from the various maps in the set of indices are then passed -to an instance of the `msg::detail::rle_intersect` object and returned from the `rle_indices` -call operator. - -The `rle_decoder` provides a single-use enumerator that will step over the groups of -`0`​s or `1`​s, providing a way to advance through them by arbitrary increments. - -The `rle_intersect` implementation wraps the variadic set of `rle_decoder`​s so that -the caller can iterate through all `1`​s, calling the appropriate callback as it goes. - -===== Efficient Iteration of Bits - -The `msg::detail::rle_decoder::chunk_enumerator` provides a way to step through the RLE -data for the encoded bitset an arbitrary number of bits at a time. It does this by exposing -the current number of bits of consecutive value. - -This is presented so that it is possible to efficiently find: - -- the longest run of `0`​s -- or, if none, the shortest run of `1`​s. - -Remember that we are trying to compute the intersection of all the encoded bitsets, so -where all bitsets have a `1`, we call the associated callback, where any of the bitsets -has a `0`, we skip that callback. - -So the `chunk_enumerator` will return a signed 16 bit (at least) value indicating: - -- *negative* value - the number of `0`​s -- *positive* value - the number of `1`​s -- *zero* when past the end (special case) - -The `rle_intersect` will initialise an array of `rle_decoder::chunk_enumerators` -when it is asked to run a lambda for each `1` bit using the `for_each()` method. - -This list is then searched for the _minimum_ value of chunk size. This will either -be the largest negative value, and so the longest run of `0`​s, or the smallest -number of `1`​s, representing the next set of bits that are set in all bitsets. - -The `for_each()` method will then advance past all the `0`​s, or execute the lambda -for that many set bits, until it has consumed all bits in the encoded bitsets. - -This means that the cost of intersection of `N` indices is a number of pointers and -a small amount of state for tracking the current run of bits and their type for each index. - -There is no need to materialise a full bitset at all. This can be quite a memory saving if -there are a large number of callbacks. The trade-off, of course, is more complex iteration -of bits to discover the callbacks to run. - diff --git a/docs/index.adoc b/docs/index.adoc index 6f3b4a23..a32df6d8 100644 --- a/docs/index.adoc +++ b/docs/index.adoc @@ -13,4 +13,3 @@ include::logging.adoc[] include::interrupts.adoc[] include::match.adoc[] include::message.adoc[] -include::implementation_details.adoc[] diff --git a/docs/intro.adoc b/docs/intro.adoc index a5e36f18..448ff2f4 100644 --- a/docs/intro.adoc +++ b/docs/intro.adoc @@ -36,7 +36,9 @@ The library dependencies are: - https://github.com/intel/cpp-std-extensions[C++ std extensions (`stdx`)] - https://github.com/intel/cpp-baremetal-concurrency[Baremetal Concurrency (`conc`)] - https://github.com/intel/cpp-senders-and-receivers[Baremetal Senders and Receivers (`async`)] -- https://github.com/fmtlib/fmt[fmt] +- https://github.com/fmtlib/fmt[fmtlib] + +NOTE: `fmtlib` is used at compile-time only; there is no runtime formatting in `cib`. === Functionality @@ -54,3 +56,23 @@ Various sub-libraries within CIB form a dependency graph. `cib` is an omnibus library that contains all the functionality. mermaid::library_deps.mmd[format="svg"] + +=== Getting Started + +`cib` is a header-only library, so you can get started by cloning the repository +(or bringing it in as a submodule) and adding the include directory to your +build target. Depending on which part(s) of `cib` you are using, you may also +need to pull in the dependencies. + +Alternatively, consuming `cib` with https://github.com/cpm-cmake/CPM.cmake[CPM] +can be a couple of lines: + +[source,cmake] +---- +cpmaddpackage("gh:intel/compile-time-init-build#1abcdef") + +target_link_libraries(my_project PUBLIC cib) +---- + +Where `1abcdef` is the git hash. You can also use any of the sub-libraries (for +example, `cib_log_binary`) in this way. diff --git a/docs/log_build_process.mmd b/docs/log_build_process.mmd new file mode 100644 index 00000000..82e88dc0 --- /dev/null +++ b/docs/log_build_process.mmd @@ -0,0 +1,30 @@ +block +columns 6 + app_cpp("main.cpp") lib_cpp("lib.cpp") space:4 + space:3 ud("undefined_symbols.txt") space:2 + space:6 + block:B:2 + columns 2 + obj("main.o") + lib("lib.a") + space + strings("strings.a") + end space + block:A:3 + gen_cpp("strings.cpp") + gen_json("strings.json") + gen_xml("strings.xml") + end + space:6 + space app("a.out") space:2 log("log decoder") space + + app_cpp-- "compile" -->obj + lib_cpp-- "compile" -->lib + lib-- "nm" -->ud + ud-- "generate" -->A + gen_cpp-- "compile" -->strings + gen_cpp-- "compile" -->strings + B-- "link + LTO" -->app + gen_json-->log + gen_xml-->log + app-- "run" -->log diff --git a/docs/logging.adoc b/docs/logging.adoc index 444711f9..fa57b673 100644 --- a/docs/logging.adoc +++ b/docs/logging.adoc @@ -6,7 +6,7 @@ https://github.com/intel/compile-time-init-build/tree/main/include/log. Everything in the logging library is in the `logging` namespace, although most user code will use macros. -Logging in _cib_ is in two parts: +Logging in `cib` is in two parts: - the interface, in https://github.com/intel/compile-time-init-build/tree/main/include/log/log.hpp[log.hpp] - an implementation, which can be specified at the top level @@ -17,9 +17,61 @@ Three possible logger implementations are provided: - one using binary encoding in https://github.com/intel/compile-time-init-build/tree/main/include/log/catalog/encoder.hpp[catalog/encoder.hpp], using the https://www.mipi.org/specifications/sys-t[MIPI Sys-T spec] by default - the default implementation: the null logger which accepts everything, but never produces output -=== Log levels +=== Getting started + +To get started using CIB logging, include `log.hpp` and use a log macro to output logs: +[source,cpp] +---- +#include + +auto func() { + CIB_INFO("Calling func"); +} +---- + +And link against the `cib_log` library: +[source,cmake] +---- +target_link_libraries(my_lib PUBLIC cib_log) +---- -_cib_ offers 6 well-known and 2 user-defined log levels, according to the https://www.mipi.org/specifications/sys-t[MIPI Sys-T spec]. +This is suitable usage for a library header. Notice: this code has not yet made +any decision about which logging implementation to use. And in fact the default +logger will produce no output. + +To select a logging implementation for an application, we need to include the +appropriate implementation header and specialize a variable template: +[source,cpp] +---- +#include +#include + +template <> +inline auto logging::config<> = + logging::fmt::config{std::ostream_iterator(std::cout)}; +---- + +And of course link against the appropriate library: +[source,cmake] +---- +target_link_libraries(my_app PUBLIC cib_log_fmt) +---- + +This will mean that the application uses the fmt logger to output to +`std::cout`. Any header-only library code that uses the log macros will +automatically use the correct logger. + +The provided fmt logger implementation can output to multiple destinations by +constructing `logging::fmt::config` with multiple `ostream` iterators. + +CAUTION: If you have multiple translation units, all TUs that use logging must +see the same specialization of the `logging::config` variable template -- +otherwise you will have an ODR violation. This includes any compiled (not +header-only) library code. + +=== Log levels +`cib` offers 6 well-known and 2 user-defined log levels, according to the +https://www.mipi.org/specifications/sys-t[MIPI Sys-T spec]. [source,cpp] ---- @@ -35,10 +87,24 @@ enum struct level { }; ---- -=== Log macros +C++ does not allow inheritance from an enumeration type, but one way to name +the user-defined levels is to use a `struct` to contain the enumeration values: -_cib_ log macros follow the log levels: +[source,cpp] +---- +struct app_level { + using enum logging::level; + constexpr static auto APP1 = USER1; +}; +---- + +There are other possibilities, but this "scopes" the enumeration values for the +application while keeping the type (`logging::level`) the same so that library +functions still work. +=== Log macros + +`cib` log macros follow the log levels: [source,cpp] ---- CIB_TRACE(...); @@ -48,33 +114,112 @@ CIB_ERROR(...); CIB_FATAL(...); ---- +These macros are defined using the lower level macro `CIB_LOG_WITH_LEVEL`, so to +provide a new macro using a user-defined log level can be done: +[source,cpp] +---- +#define LOG_APP1(...) CIB_LOG_WITH_LEVEL(app_level::APP1 __VA_OPT__(, ) __VA_ARGS__) +---- + `CIB_FATAL` causes a call to https://intel.github.io/cpp-std-extensions/#_panic_hpp[`stdx::panic`], and `CIB_ASSERT(expression)` is equivalent to `CIB_FATAL` in the case where the expression evaluates to `false`. -=== Selecting a logger +=== Log formatting +Under the hood, `cib` uses https://github.com/fmtlib/fmt[`fmt`] to format logs +before outputting them. All formatting that can be done at compile time, is done +at compile time. Some examples: -In order to use logging in a header, it suffices only to include -https://github.com/intel/compile-time-init-build/tree/main/include/log/log.hpp[log.hpp] -and use the macros. Header-only clients of logging do not need to know the -implementation selected. +[source,cpp] +---- +CIB_INFO("The answer is: {}", 42); // compile-time formatted +CIB_INFO("The answer is: {}", "42"); // compile-time formatted -To use logging in a translation unit, specialize the `logging::config` variable -template. Left unspecialized, the null logger will be used. +static constexpr auto x = 42; +CIB_INFO("The answer is: {}", x); // compile-time formatted +CIB_INFO("42 is an {}", 42, int); // compile-time formatted +auto y = 42; +CIB_INFO("The answer is: {}", y); // runtime formatted +CIB_INFO("{} is an {}", y, int); // compile-time (int) and runtime (y) formatted +---- + +NOTE: We can pass types as well as values to the log macros for formatting. +Types are converted to string representations. + +=== Binary loggers +An application can select the binary logger by specializing the +`logging::config` variable template and providing a destination that will +receive binary data: [source,cpp] ---- -// use fmt logging to std::cout +#include +#include + +struct log_destination { + template + auto operator()(stdx::span data) const {} +}; + template <> -inline auto logging::config<> = logging::fmt::config{std::ostream_iterator{std::cout}}; +inline auto logging::config<> = logging::binary::config{log_destination{}}; +---- + +And linking against the binary log library: +[source,cmake] ---- +target_link_libraries(my_app PUBLIC cib_log_binary) +---- + +The log destination must provide a function call operator (`operator()`) that +receives a span of the binary data to be written. + +==== The build process + +On a constrained system, space for text can be limited-to-nonexistent. The +`cib_log_binary` library encodes strings at build time so that string IDs are +sent at runtime and string data is not stored in the executable. -The provided `fmt` implementation can output to multiple destinations by constructing -`logging::fmt::config` with multiple `ostream` iterators. +- First, each string constant contains string character data in its type. +- The binary logger calls the function template specialization + https://github.com/intel/compile-time-init-build/blob/main/include/log/catalog/catalog.hpp[`catalog`] + to get the ID corresponding to each string constant. -CAUTION: Be sure that each translation unit sees the same specialization of -`logging::config`! Otherwise you will have an https://en.cppreference.com/w/cpp/language/definition[ODR violation]. +But: the `catalog` function template is just that -- only a template -- to +begin with. It is specialized as follows: + +- The application is built as a library. +- Running `nm` on that library reveals missing symbols: precisely the function + specializations that are required for all the string constants. +- Those symbols are used to generate the template specializations in another + file, which itself is compiled into a library. +- String data is recovered from the symbol types and used to generate the + catalog collateral in XML and/or JSON format. +- Link-time optimization inlines the `catalog` function template + specializations, each of which is a one-line function that returns an ID. + +mermaid::log_build_process.mmd[format="svg"] + +NOTE: No logging exists in `main.cpp`: it's just a stub providing `main` that +calls into the application's library code. + +Thus no string data exists in the executable, but the correct IDs are used for +logging, and at runtime a log decoder can reconstitute the actual strings. The +XML and JSON collateral also contains information about any runtime arguments +that need to be interpolated into the string and whose values are sent by the +binary logger along with the ID. + +==== Tooling support + +The process of generating log strings from the type information revealed by +missing symbols is automated by a +https://github.com/intel/compile-time-init-build/blob/main/tools/gen_str_catalog.py[python +script] provided and by a +https://github.com/intel/compile-time-init-build/blob/main/cmake/string_catalog.cmake[CMake +wrapper function (`gen_str_catalog`)] that drives the process. See +https://github.com/intel/compile-time-init-build/blob/main/test/CMakeLists.txt[the +test] that exercises that functionality for an example. === Implementing a logger @@ -86,8 +231,8 @@ implementation is a matter of defining this structure appropriately. ---- struct my_logger_config { struct { - template - auto log(File, Line, Msg const &msg) -> void { + template + auto log(File, Line, FR const &fr) -> void { // log according to my mechanism } } logger; @@ -97,30 +242,31 @@ struct my_logger_config { Notice that the first template parameters to log is the xref:logging.adoc#_logging_environments[environment]. -The first two runtime parameters receive preprocessor `\_​_FILE_​\_` and `__LINE_​_` values -respectively. The `msg` argument is a structure containing a -compile-time format string and runtime -arguments to be interpolated into it. It supports an `apply` function, so one -way to implement `log` is: +The first two runtime parameters receive preprocessor `\_​_FILE_​\_` and +`__LINE_​_` values respectively. The `fr` argument is a +https://intel.github.io/cpp-std-extensions/#_ct_format_hpp[`stdx::format_result`] +structure containing a +https://intel.github.io/cpp-std-extensions/#_cts_t[compile-time format string] +and runtime arguments to be interpolated into it. One way to implement `log` is: [source,cpp] ---- struct my_logger_config { struct { - template - auto log(File, Line, Msg const &msg) -> void { - msg.apply([] (Str, auto const&... args) { - std::print(Str::value, args...); + template + auto log(File, Line, FR const &fr) -> void { + constexpr auto fmtstr = std::string_view{decltype(fr.str)::value}; + fr.args.apply([&](auto const &...args) { + ::fmt::print(fmtstr, args...); }); } } logger; }; ---- -NOTE: `Str::value` here is a compile-time `std::string_view`. - -To use the custom implementation, as with any built-in choice of logger, -specialize `logging::config`: +In fact this is similar to how the `fmt` logger is implemented. To use the +custom implementation, as with any built-in choice of logger, specialize +`logging::config`: [source,cpp] ---- @@ -142,13 +288,16 @@ template <> inline auto logging::config = my_logger_config{}; ---- -And this backend can be most easily used by defining macros in terms of the -`CIB_LOG` macro: +And this backend can be used by defining macros in terms of the `logging::log` +function: [source,cpp] ---- -#define SECURE_TRACE(...) CIB_LOG(secure_tag, logging::level::TRACE, __VA_ARGS__) -#define SECURE_INFO(...) CIB_LOG(secure_tag, logging::level::INFO, __VA_ARGS__) +#define SECURE_TRACE(MSG, ...) \ + logging::log{}>>( \ + __FILE__, __LINE__, stdx::ct_format(__VA_ARGS__)) + // etc ---- @@ -157,7 +306,7 @@ And this backend can be most easily used by defining macros in terms of the It can be helpful to scope or filter log messages by associating them with module IDs. Several logging backends have support for this idea. Tagging every log call site gets verbose and error-prone, so instead the approach taken by -_cib_ is to override log modules by using `CIB_LOG_MODULE` declarations at +`cib` is to override log modules by using `CIB_LOG_MODULE` declarations at namespace, class or function scope. [source,cpp] @@ -185,53 +334,6 @@ struct my_struct { } ---- -=== Efficient logging with MIPI Sys-T - -On a constrained system, space for text can be limited-to-nonexistent. `cib` -uses `stdx::ct_format` and the -https://github.com/intel/compile-time-init-build/tree/main/include/log/catalog/mipi_encoder.hpp[MIPI -Sys-T logging config] to solve this problem. - -- First, each string constant contains string character data in its type. -- The MIPI logger calls the function template specialization - https://github.com/intel/compile-time-init-build/blob/main/include/log/catalog/catalog.hpp[`catalog`] - to get the catalog ID corresponding to each string constant. - -But: the `catalog` function template is just that -- only a template -- to -begin with. It is specialized as follows: - -- The application is built as a library. -- Running `nm` on that library reveals missing symbols: precisely the function - specializations that are required for all the string constants. -- Those symbols are used to generate the template specializations in another - file, which itself is compiled into a library. -- String data is recovered from the symbol types and used to generate the - catalog collateral in XML and/or JSON format. -- Link-time optimization inlines the `catalog` function template - specializations, each of which is a one-line function that returns a - catalog ID. - -Thus no string data exists in the executable, but the correct catalog IDs are -used in logging, and the remote log handler can reconstitute the actual strings. -The XML and JSON collateral also contains information about any runtime -arguments that need to be interpolated into the string and whose values are sent -by the MIPI Sys-T logger after the catalog ID. - -==== Tooling support - -The process of generating log strings from the type information revealed by -missing symbols is automated by a -https://github.com/intel/compile-time-init-build/blob/main/tools/gen_str_catalog.py[python -script] provided and by a -https://github.com/intel/compile-time-init-build/blob/main/cmake/string_catalog.cmake[CMake -wrapper function (`gen_str_catalog`)] that drives the process. See -https://github.com/intel/compile-time-init-build/blob/main/test/CMakeLists.txt[the -test] that exercises that functionality for an example. - -NOTE: This process assigns IDs to both strings and -xref:logging.adoc#_modules[log modules]. `catalog` is specialized for catalog -IDs; `module` is specialized for module IDs. - === Version logging To provide version information in a log, specialize the `version::config` @@ -298,7 +400,7 @@ CIB_TRACE("Hello"); // logs with secure back end A temporary override of values can be done with `CIB_WITH_LOG_ENV`: [source,cpp] ---- -CIB_WITH LOG_ENV(logging::get_level, logging::level::TRACE, +CIB_WITH_LOG_ENV(logging::get_level, logging::level::TRACE, logging::get_flavor, secure_tag) { CIB_LOG("Hello"); // logs a TRACE with secure back end } @@ -310,11 +412,49 @@ on the environment. ---- struct my_logger_config { struct { - template - auto log(File, Line, Msg const &msg) -> void { + template + auto log(File, Line, FR const &fr) -> void { constexpr auto level = get_level(Env{}).value; // ... } } logger; }; ---- + +=== Queries + +Much of the behavior of a log call can be customized using values queried from +an environment: + +- https://github.com/intel/compile-time-init-build/blob/main/include/log/level.hpp[severity] +- https://github.com/intel/compile-time-init-build/blob/main/include/log/flavor.hpp[flavor] (typically secure or otherwise) +- https://github.com/intel/compile-time-init-build/blob/main/include/log/module.hpp[module] +- https://github.com/intel/compile-time-init-build/blob/main/include/log/unit.hpp[unit] +- https://github.com/intel/compile-time-init-build/blob/main/include/log/string_id.hpp[string ID] or https://github.com/intel/compile-time-init-build/blob/main/include/log/module_id.hpp[module ID] +- https://github.com/intel/compile-time-init-build/blob/main/include/log/catalog/builder.hpp[binary builder] +- https://github.com/intel/compile-time-init-build/blob/main/include/log/catalog/writer.hpp[binary writer] + +The binary logger in particular can have both the builder and the writer +(destination) customized in this way. The defaults provide binary logging +according to the https://www.mipi.org/specifications/sys-t[MIPI Sys-T spec], but +any binary format can be provided by customizing the builder. + +The string ID or module ID can also be fixed for a particular call, and the ID +generation process will heed IDs fixed in this way. + +==== Runtime queries + +If a query value is not known at compile time, it may be provided at runtime by +a function (non-capturing lambda expression). This is useful for the `unit` +value which is somewhat similar to `module`, but primarily intended to +distinguish between multiple runtime instances. + +[source,cpp] +---- +logging::mipi::unit_t my_unit = discover_unit(); +CIB_LOG_ENV(logging::get_unit, [] { return my_unit; }); +CIB_TRACE("Hello"); +---- + +See the https://www.mipi.org/specifications/sys-t[MIPI Sys-T spec] for more +details.