From 09af56f99a480621fa9714e8325199f13ea2eab5 Mon Sep 17 00:00:00 2001 From: Jo5ta Date: Sat, 11 Jul 2026 01:14:26 +0200 Subject: [PATCH 1/4] feat: fmt-style tracepoints with compile-time validated {} formats CLLTK_TRACEPOINT_FMT(buffer, "loaded {} in {}ms", name, ms) accepts C++20 std::format placeholders. Validation happens at compile time through a never-executed call to a helper taking std::format_string, whose consteval constructor checks the literal against the argument types - stronger than the printf attribute. Argument encoding is unchanged (the library types arguments independently of format syntax); the new meta entry type 5 only marks the syntax for decoders. The varargs serializer is refactored around a va_list core so fmt tracepoints get an entry point without the printf format attribute ({} strings must not be checked as printf formats). A char* argument is always recorded as a string - {} has no printf %p/%s ambiguity. C and kernel: not available (no compile-time {} validation exists there); the macro fails with a static assert. Signed-off-by: Jo5ta --- .../tracing/_internal.h | 10 +++ .../tracing/_kernel_tracing.h | 6 ++ .../CommonLowLevelTracingKit/tracing/_meta.h | 1 + .../tracing/_user_tracing.h | 61 +++++++++++++++++++ .../tracing/tracing.h | 14 +++++ tracing_library/source/tracepoint.c | 40 +++++++++--- 6 files changed, 125 insertions(+), 7 deletions(-) diff --git a/tracing_library/include/CommonLowLevelTracingKit/tracing/_internal.h b/tracing_library/include/CommonLowLevelTracingKit/tracing/_internal.h index ea76f0f..db897d3 100644 --- a/tracing_library/include/CommonLowLevelTracingKit/tracing/_internal.h +++ b/tracing_library/include/CommonLowLevelTracingKit/tracing/_internal.h @@ -78,6 +78,16 @@ void _clltk_tracebuffer_register_metaptrs(_clltk_tracebuffer_handler_t *buffer, const void *const *pairs_stop) __attribute__((nonnull(1), used, visibility("default"))); +/* identical to _clltk_static_tracepoint_with_args but without the printf + * format attribute: used by fmt-style tracepoints whose format strings use + * {} placeholders and must not be checked as printf formats */ +void _clltk_static_tracepoint_with_args_unchecked(_clltk_tracebuffer_handler_t *buffer, + const _clltk_file_offset_t in_file_offset, + const char *const file, const uint32_t line, + _clltk_argument_types_t *types, + const char *const format, ...) + __attribute__((nonnull(1, 3, 5), used, visibility("default"))); + /* span events carry fixed uint64 arguments and no printf format, so they use * their own entry points instead of the format-checked varargs function */ void _clltk_static_tracepoint_span_begin(_clltk_tracebuffer_handler_t *buffer, diff --git a/tracing_library/include/CommonLowLevelTracingKit/tracing/_kernel_tracing.h b/tracing_library/include/CommonLowLevelTracingKit/tracing/_kernel_tracing.h index 85c1102..e97929b 100644 --- a/tracing_library/include/CommonLowLevelTracingKit/tracing/_kernel_tracing.h +++ b/tracing_library/include/CommonLowLevelTracingKit/tracing/_kernel_tracing.h @@ -86,6 +86,12 @@ __attribute__((destructor(101), used)) static void _clltk_destructor(void) &_clltk_types, _FORMAT_ _CLLTK_CAST(__VA_ARGS__)); \ } while (0) +/* fmt-style tracepoints are C++20 userspace only */ +#define _CLLTK_STATIC_TRACEPOINT_FMT(_BUFFER_, _FORMAT_, ...) \ + do { \ + _CLLTK_STATIC_ASSERT(0, "CLLTK_TRACEPOINT_FMT is not available in kernel"); \ + } while (0) + /* shared compile-time core for span events in kernel modules; mirrors the * userspace variant with the kernel meta proxy registration */ #define _CLLTK_STATIC_SPAN_EVENT(_TYPE_, _BUFFER_, _NAME_, _CALL_, ...) \ diff --git a/tracing_library/include/CommonLowLevelTracingKit/tracing/_meta.h b/tracing_library/include/CommonLowLevelTracingKit/tracing/_meta.h index 1fd45ff..4669be7 100644 --- a/tracing_library/include/CommonLowLevelTracingKit/tracing/_meta.h +++ b/tracing_library/include/CommonLowLevelTracingKit/tracing/_meta.h @@ -26,6 +26,7 @@ typedef enum __attribute__((packed)) { _clltk_meta_enty_type_dump = 2, _clltk_meta_enty_type_span_begin = 3, _clltk_meta_enty_type_span_end = 4, + _clltk_meta_enty_type_fmt = 5, } _clltk_meta_enty_type; _CLLTK_STATIC_ASSERT(sizeof(_clltk_meta_enty_type) == 1, "should be of size 1"); diff --git a/tracing_library/include/CommonLowLevelTracingKit/tracing/_user_tracing.h b/tracing_library/include/CommonLowLevelTracingKit/tracing/_user_tracing.h index 3265c7b..4c03eb2 100644 --- a/tracing_library/include/CommonLowLevelTracingKit/tracing/_user_tracing.h +++ b/tracing_library/include/CommonLowLevelTracingKit/tracing/_user_tracing.h @@ -24,6 +24,22 @@ #define _CLLTK_TRACEBUFFER_MACRO_VALUE(_NAME_) _NAME_ +/* fmt-style tracepoints ({} placeholders) are C++20 only: std::format_string + * validates the format against the argument types at compile time. */ +#if defined(CLLTK_FOR_CPP) && defined(__has_include) +#if __has_include() +#include +#include +#if defined(__cpp_lib_format) +#define _CLLTK_HAS_FMT 1 +template +inline void _clltk_fmt_check(std::format_string...>, _Args &&...) +{ +} +#endif +#endif +#endif + #if !defined(_CLLTK_INTERNAL) _CLLTK_EXTERN_C_BEGIN @@ -176,6 +192,51 @@ _CLLTK_EXTERN_C_END _FORMAT_ _CLLTK_CAST(__VA_ARGS__)); \ } while (0) +#if defined(_CLLTK_HAS_FMT) +#define _CLLTK_STATIC_TRACEPOINT_FMT(_BUFFER_, _FORMAT_, ...) \ + do { \ + /* ------- compile time stuff ------- */ \ + if (false) { /* never executed: validates {} format against arg types */ \ + _clltk_fmt_check(_FORMAT_ __VA_OPT__(, ) __VA_ARGS__); \ + } \ + _CLLTK_STATIC_ASSERT(_CLLTK_NARGS(__VA_ARGS__) <= 10, \ + "only supporting up to 10 arguments"); \ + _CLLTK_CHECK_FOR_ARGUMENTS(__VA_ARGS__); \ + \ + /* create meta data for this tracepoint, the per-call-site offset */ \ + /* cache (filled by the startup registration), and a discovery entry */ \ + static _clltk_file_offset_t _clltk_offset = _clltk_file_offset_unset; \ + _CLLTK_CREATE_META_ENTRY_TYPED(_meta, _CLLTK_PLACE_IN(_BUFFER_), \ + _clltk_meta_enty_type_fmt, _FORMAT_, __VA_ARGS__); \ + _CLLTK_EMIT_META_PTR(_BUFFER_, _meta, _clltk_offset); \ + \ + static _clltk_argument_types_t _clltk_types = _CLLTK_CREATE_TYPES(__VA_ARGS__); \ + \ + static _clltk_tracebuffer_handler_t *const _tb = &_clltk_##_BUFFER_; \ + \ + /* ------- runtime time stuff ------- */ \ + \ + if ((_tb->runtime.tracebuffer == NULL)) { \ + if (!_clltk_tracebuffer_init(_tb)) { \ + break; \ + } \ + } \ + \ + if (_clltk_offset == _clltk_file_offset_unset) { \ + _clltk_offset = _clltk_tracebuffer_get_in_file_offset(_tb, &_meta, sizeof(_meta)); \ + } \ + \ + _clltk_static_tracepoint_with_args_unchecked(_tb, _clltk_offset, __FILE__, __LINE__, \ + &_clltk_types, \ + _FORMAT_ _CLLTK_CAST(__VA_ARGS__)); \ + } while (0) +#else +#define _CLLTK_STATIC_TRACEPOINT_FMT(_BUFFER_, _FORMAT_, ...) \ + do { \ + _CLLTK_STATIC_ASSERT(0, "CLLTK_TRACEPOINT_FMT requires C++20 with "); \ + } while (0) +#endif + /* shared compile-time core for span events: meta entry with the given type, * discovery entry, tracebuffer init, and resolved file offset. The * placeholder arguments only determine the argument type array of the meta diff --git a/tracing_library/include/CommonLowLevelTracingKit/tracing/tracing.h b/tracing_library/include/CommonLowLevelTracingKit/tracing/tracing.h index 93d0206..ddca4a8 100644 --- a/tracing_library/include/CommonLowLevelTracingKit/tracing/tracing.h +++ b/tracing_library/include/CommonLowLevelTracingKit/tracing/tracing.h @@ -56,6 +56,20 @@ static tracepoint #define CLLTK_TRACEPOINT_DUMP(_BUFFER_, _MSG_, _ADDRESS_, _SIZE_) \ _CLLTK_STATIC_TRACEPOINT_DUMP(_BUFFER_, _MSG_, _ADDRESS_, _SIZE_) +/* +fmt-style static tracepoint (C++20 only) +- format strings use {} placeholders, validated against the argument types at + compile time via std::format_string +- same speed and mechanics as CLLTK_TRACEPOINT, only the format syntax differs +- a char* argument is always recorded as a string ({} has no printf-style + %p/%s ambiguity); cast to void* to record the pointer value + +example: + CLLTK_TRACEPOINT_FMT(some_tracebuffer, "loaded {} in {}ms", name, duration); +*/ +#define CLLTK_TRACEPOINT_FMT(_BUFFER_, _FORMAT_, ...) \ + _CLLTK_STATIC_TRACEPOINT_FMT(_BUFFER_, _FORMAT_ __VA_OPT__(, ) __VA_ARGS__) + /* span tracking with a carryable id - CLLTK_SPAN_BEGIN records a span-begin event and evaluates to the new span id diff --git a/tracing_library/source/tracepoint.c b/tracing_library/source/tracepoint.c index ae499e5..1080cfb 100644 --- a/tracing_library/source/tracepoint.c +++ b/tracing_library/source/tracepoint.c @@ -114,11 +114,11 @@ EXPORT_SYMBOL(_clltk_static_tracepoint_span_end); _CLLTK_PRAGMA_DIAG(push) _CLLTK_PRAGMA_DIAG(ignored "-Wstack-protector") -void _clltk_static_tracepoint_with_args(_clltk_tracebuffer_handler_t *handler, - const _clltk_file_offset_t in_file_offset, - const char *const file, const uint32_t line, - _clltk_argument_types_t *types, const char *const format, - ...) +static void static_tracepoint_with_args_v(_clltk_tracebuffer_handler_t *handler, + const _clltk_file_offset_t in_file_offset, + const char *const file, const uint32_t line, + _clltk_argument_types_t *types, const char *const format, + va_list args_in) { if (unlikely(false == _CLLTK_FILE_OFFSET_IS_STATIC(in_file_offset))) { ERROR_LOG("invalid in_file_offset(%llu) at %s:%d for %s", @@ -139,9 +139,8 @@ void _clltk_static_tracepoint_with_args(_clltk_tracebuffer_handler_t *handler, }; size_t raw_entry_size = sizeof(traceentry_head); - // open va_arg va_list args; - va_start(args, format); + va_copy(args, args_in); uint32_t arg_sizes[10] = {0}; raw_entry_size += get_argument_sizes(format, arg_sizes, types, args); @@ -176,10 +175,37 @@ void _clltk_static_tracepoint_with_args(_clltk_tracebuffer_handler_t *handler, memory_heap_free(raw_entry_buffer); } } + +void _clltk_static_tracepoint_with_args(_clltk_tracebuffer_handler_t *handler, + const _clltk_file_offset_t in_file_offset, + const char *const file, const uint32_t line, + _clltk_argument_types_t *types, const char *const format, + ...) +{ + va_list args; + va_start(args, format); + static_tracepoint_with_args_v(handler, in_file_offset, file, line, types, format, args); + va_end(args); +} #ifdef __KERNEL__ EXPORT_SYMBOL(_clltk_static_tracepoint_with_args); #endif +void _clltk_static_tracepoint_with_args_unchecked(_clltk_tracebuffer_handler_t *handler, + const _clltk_file_offset_t in_file_offset, + const char *const file, const uint32_t line, + _clltk_argument_types_t *types, + const char *const format, ...) +{ + va_list args; + va_start(args, format); + static_tracepoint_with_args_v(handler, in_file_offset, file, line, types, format, args); + va_end(args); +} +#ifdef __KERNEL__ +EXPORT_SYMBOL(_clltk_static_tracepoint_with_args_unchecked); +#endif + void _clltk_static_tracepoint_with_dump(_clltk_tracebuffer_handler_t *handler, const _clltk_file_offset_t in_file_offset, const char *const file, const uint32_t line, From 5f4309d424e7f4412806de8655edb9d964669203 Mon Sep 17 00:00:00 2001 From: Jo5ta Date: Sat, 11 Jul 2026 01:14:26 +0200 Subject: [PATCH 2/4] decoder: render fmt tracepoints in both decoders Meta entry type 5 renders through the decoders' native {} formatters: python str.format, C++ std::vformat. Arguments decode with the existing type-array machinery including foreign-endian swapping; the printf/ffi path is never touched for fmt entries. Formatting errors degrade to the raw format string instead of failing the decode. The C++ implementation wraps each decoded value in a small type with a custom std::formatter that forwards the format spec, avoiding the combinatorial std::visit-into-vformat instantiation blowup a naive variant approach causes. Signed-off-by: Jo5ta --- .../CommonLowLevelTracingKit/decoder/Meta.hpp | 3 +- decoder_tool/cpp/source/Tracepoint.cpp | 4 +- .../cpp/source/TracepointInternal.hpp | 5 +- .../cpp/source/low_level/formatter.cpp | 110 ++++++++++++++++++ .../cpp/source/low_level/formatter.hpp | 2 + .../cpp/source/low_level/meta_parser.cpp | 3 + decoder_tool/python/clltk_decoder.py | 38 +++++- 7 files changed, 160 insertions(+), 5 deletions(-) diff --git a/decoder_tool/cpp/include/CommonLowLevelTracingKit/decoder/Meta.hpp b/decoder_tool/cpp/include/CommonLowLevelTracingKit/decoder/Meta.hpp index fceed04..7c56857 100644 --- a/decoder_tool/cpp/include/CommonLowLevelTracingKit/decoder/Meta.hpp +++ b/decoder_tool/cpp/include/CommonLowLevelTracingKit/decoder/Meta.hpp @@ -20,7 +20,8 @@ namespace CommonLowLevelTracingKit::decoder { Printf = 1, Dump = 2, SpanBegin = 3, - SpanEnd = 4 + SpanEnd = 4, + Fmt = 5 }; struct EXPORT MetaEntryInfo { diff --git a/decoder_tool/cpp/source/Tracepoint.cpp b/decoder_tool/cpp/source/Tracepoint.cpp index 1510879..5f0e721 100644 --- a/decoder_tool/cpp/source/Tracepoint.cpp +++ b/decoder_tool/cpp/source/Tracepoint.cpp @@ -136,6 +136,8 @@ const std::string_view TracepointStatic::msg() const { std::span args_raw{arg_start, arg_size}; if (m_type == MetaType::printf) [[likely]] { m_msg = source::formatter::printf(format(), m_arg_types, args_raw, e->foreignEndian()); + } else if (m_type == MetaType::fmt) { + m_msg = source::formatter::fmt(format(), m_arg_types, args_raw, e->foreignEndian()); } else if (m_type == MetaType::dump) { m_msg = source::formatter::dump(format(), m_arg_types, args_raw, e->foreignEndian()); } else if (m_type == MetaType::span_begin) { @@ -161,7 +163,7 @@ const std::string_view TracepointStatic::msg() const { CLLTK_DECODER_THROW( exception::InvalidMeta, "Invalid meta data type: " + std::to_string(static_cast(m_type)) + - " (expected printf=1, dump=2, span_begin=3, or span_end=4)"); + " (expected printf=1, dump=2, span_begin=3, span_end=4, or fmt=5)"); } } return m_msg; diff --git a/decoder_tool/cpp/source/TracepointInternal.hpp b/decoder_tool/cpp/source/TracepointInternal.hpp index 2d2249f..7f18126 100644 --- a/decoder_tool/cpp/source/TracepointInternal.hpp +++ b/decoder_tool/cpp/source/TracepointInternal.hpp @@ -106,10 +106,11 @@ namespace CommonLowLevelTracingKit::decoder { printf = 1, dump = 2, span_begin = 3, - span_end = 4 + span_end = 4, + fmt = 5 }; static constexpr CONST_INLINE MetaType toMetaType(uint8_t a) { - if (a <= static_cast(MetaType::span_end)) return static_cast(a); + if (a <= static_cast(MetaType::fmt)) return static_cast(a); return MetaType::undefined; } class TracepointStatic final : public TraceEntryHead { diff --git a/decoder_tool/cpp/source/low_level/formatter.cpp b/decoder_tool/cpp/source/low_level/formatter.cpp index 9ae6e5f..79f3dac 100644 --- a/decoder_tool/cpp/source/low_level/formatter.cpp +++ b/decoder_tool/cpp/source/low_level/formatter.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include @@ -337,6 +338,115 @@ std::string formatter::printf(const std::string_view format, const std::span; + +// Read a POD value of type T from raw memory at clltk_arg, byte-swapping if needed. +template +INLINE static T read_raw(uintptr_t clltk_arg, size_t remaining, bool foreign_endian) { + if (sizeof(T) > remaining) [[unlikely]] + CLLTK_DECODER_THROW(FormattingFailed, "out of range access for fmt formatter"); + T value{}; + memcpy(&value, reinterpret_cast(clltk_arg), sizeof(T)); + if constexpr (CommonLowLevelTracingKit::decoder::source::internal::ByteSwappable) { + if (foreign_endian) + value = CommonLowLevelTracingKit::decoder::source::internal::byteswapValue(value); + } + return value; +} + +INLINE static fmt_arg_value clltk_arg_to_fmt_value(const char clltk_type, const uintptr_t clltk_arg, + size_t remaining, bool foreign_endian) { + switch (clltk_type) { + case 'c': return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'w': + return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'i': + return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'l': + return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'C': return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'W': return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'I': return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'L': return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'f': return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'd': return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 'p': + return static_cast(read_raw(clltk_arg, remaining, foreign_endian)); + case 's': { + // string: uint32 length prefix then null-terminated chars; skip the length prefix + return std::string{reinterpret_cast(clltk_arg + sizeof(uint32_t))}; + } + default: CLLTK_DECODER_THROW(FormattingFailed, "unknown type for fmt"); + } +} + +// Wrapper so one std::formatter specialization can format any decoded value. +// Dispatching the concrete types at the make_format_args call site would need +// nested std::visit over all argument slots, instantiating vformat for every +// type combination (4^10). Instead the wrapper's formatter captures the format +// spec during parse() and applies it to the variant's active alternative with +// a single std::visit per argument. +struct FmtDecodedArg { + fmt_arg_value value{}; +}; + +template <> struct std::formatter { + std::string spec; + + constexpr auto parse(std::format_parse_context &ctx) { + auto it = ctx.begin(); + const auto end = ctx.end(); + while (it != end && *it != '}') { spec += *it++; } + return it; + } + + auto format(const FmtDecodedArg &arg, std::format_context &ctx) const { + const std::string one_arg_format = "{:" + spec + "}"; + return std::visit( + [&](const auto &value) { + return std::vformat_to(ctx.out(), one_arg_format, std::make_format_args(value)); + }, + arg.value); + } +}; + +std::string formatter::fmt(const std::string_view format, const std::span &types_raw, + const std::span &args_raw, bool foreign_endian) { + if (format.empty()) return ""; + + // Decode all arguments into a variant array so we have lvalues for make_format_args. + static constexpr size_t max_fmt_args = 10; + std::array decoded{}; + const size_t arg_count = types_raw.size(); + if (arg_count > max_fmt_args) CLLTK_DECODER_THROW(FormattingFailed, "too many fmt args"); + + size_t raw_offset = 0; + for (size_t i = 0; i < arg_count; ++i) { + const char type = types_raw[i]; + if (raw_offset >= args_raw.size()) + CLLTK_DECODER_THROW(FormattingFailed, "out of range access for fmt formatter"); + const uintptr_t current = std::bit_cast(&args_raw[raw_offset]); + const size_t remaining = args_raw.size() - raw_offset; + // Validate and compute size first (clltk_arg_to_size checks string bounds). + const size_t arg_size = clltk_arg_to_size(type, current, remaining, foreign_endian); + decoded[i].value = clltk_arg_to_fmt_value(type, current, remaining, foreign_endian); + raw_offset += arg_size; + } + if (raw_offset != args_raw.size()) [[unlikely]] + CLLTK_DECODER_THROW(FormattingFailed, "raw args invalid"); + + // Pass all slots; std::vformat ignores arguments the format string does not + // reference, so the unused trailing (default-constructed) slots are harmless. + try { + return std::vformat(format, + std::make_format_args(decoded[0], decoded[1], decoded[2], decoded[3], + decoded[4], decoded[5], decoded[6], decoded[7], + decoded[8], decoded[9])); + } catch (const std::format_error &) { return std::string(format) + " fmt-error"; } +} + std::string formatter::dump(const std::string_view format, const std::span &types_raw, const std::span &args_raw, bool foreign_endian) { if (types_raw.size() != 1 || types_raw[0] != 'x') diff --git a/decoder_tool/cpp/source/low_level/formatter.hpp b/decoder_tool/cpp/source/low_level/formatter.hpp index 955b53c..699fedd 100644 --- a/decoder_tool/cpp/source/low_level/formatter.hpp +++ b/decoder_tool/cpp/source/low_level/formatter.hpp @@ -13,5 +13,7 @@ namespace CommonLowLevelTracingKit::decoder::source::formatter { const std::span &args_raw, bool foreign_endian = false); std::string dump(const std::string_view format, const std::span &types, const std::span &args_raw, bool foreign_endian = false); + std::string fmt(const std::string_view format, const std::span &types, + const std::span &args_raw, bool foreign_endian = false); } // namespace CommonLowLevelTracingKit::decoder::source::formatter \ No newline at end of file diff --git a/decoder_tool/cpp/source/low_level/meta_parser.cpp b/decoder_tool/cpp/source/low_level/meta_parser.cpp index 25fbc22..9243faf 100644 --- a/decoder_tool/cpp/source/low_level/meta_parser.cpp +++ b/decoder_tool/cpp/source/low_level/meta_parser.cpp @@ -60,6 +60,8 @@ namespace CommonLowLevelTracingKit::decoder::source { entry.type = MetaEntryType::SpanBegin; } else if (type_byte == static_cast(MetaEntryType::SpanEnd)) { entry.type = MetaEntryType::SpanEnd; + } else if (type_byte == static_cast(MetaEntryType::Fmt)) { + entry.type = MetaEntryType::Fmt; } else { entry.type = MetaEntryType::Unknown; } @@ -106,6 +108,7 @@ namespace CommonLowLevelTracingKit::decoder { case MetaEntryType::Dump: return "dump"; case MetaEntryType::SpanBegin: return "span_begin"; case MetaEntryType::SpanEnd: return "span_end"; + case MetaEntryType::Fmt: return "fmt"; case MetaEntryType::Unknown: default: return "unknown"; } diff --git a/decoder_tool/python/clltk_decoder.py b/decoder_tool/python/clltk_decoder.py index eb25301..7ae39cf 100755 --- a/decoder_tool/python/clltk_decoder.py +++ b/decoder_tool/python/clltk_decoder.py @@ -30,6 +30,7 @@ class MetaEntryType(Enum): Dump = 2 SpanBegin = 3 SpanEnd = 4 + Fmt = 5 class Endian(Enum): Big = "big" @@ -436,7 +437,8 @@ def __init__(self, entry: Ringbuffer_Entry, meta: Stack_Entry): self.type = get_int(meta.body, self.metaentry_in_metablob + 5, 1) assert self.type in [MetaEntryType.Printf.value, MetaEntryType.Dump.value, - MetaEntryType.SpanBegin.value, MetaEntryType.SpanEnd.value], \ + MetaEntryType.SpanBegin.value, MetaEntryType.SpanEnd.value, + MetaEntryType.Fmt.value], \ f"invalid meta data, unsupported type {self.type}" self.meta_size = get_int(meta.body, self.metaentry_in_metablob + 1, 4) @@ -501,6 +503,40 @@ def __init__(self, entry: Ringbuffer_Entry, meta: Stack_Entry): self.args = [span_id] return + # Type 5 (Fmt): C++20 std::format-style {} placeholders. + # Supported subset: positional {} in order, and format specs that + # Python's str.format also understands ({:x}, {:08.3f}, {:>10}, etc.). + # Specs are passed through unchanged to str.format. + # Arguments are encoded identically to printf (type 1) entries; char* + # args are always length-prefixed strings (never pointer form). + # Falls back to " >" when str.format raises + # for specs outside the common subset, so no crash on unknown specs. + if self.type == MetaEntryType.Fmt.value: + self.args = [] + # Synthetic format dict for get_arg: char* is always a string in + # fmt mode (no %p variant), so pass conversion_specifier='s'. + _fmt_format_dict = {"conversion_specifier": "s"} + try: + offset = 0 + for i in range(self.argument_count): + (offset, value) = StaticTraceentry.get_arg( + entry.args_raw, offset, self.arg_types[i], _fmt_format_dict) + self.args.append(value) + except Exception as e: + error_str = (f'Extracting fmt argument failed "{self.format}" ' + f'from {self.file}:{self.line} with ' + f'{e.with_traceback(e.__traceback__)} in {traceback.format_exc()}') + logging.error(error_str) + self.formatted = error_str + return + logging.debug(f"fmt tp args = {self.args}") + try: + self.formatted = self.format.format(*self.args) if self.argument_count else self.format + except Exception as e: + self.formatted = f"fmt-error: {self.format} {self.args}" + logging.error(f'fmt format failed "{self.format}" from {self.file}:{self.line} with {e}') + return + formats = [m.groupdict() for m in format_regex.finditer(self.format)] self.args = [] From 84544943531af0dab0989f659158fdf2cea17c98 Mon Sep 17 00:00:00 2001 From: Jo5ta Date: Sat, 11 Jul 2026 01:14:26 +0200 Subject: [PATCH 3/4] test: fmt coverage and golden fixtures; harness to C++20 Meta type mapping unit tests cover types 3-5. The integration test compiles, runs, and decodes fmt tracepoints including format specs ({:x}, {:.2f}). Golden 1.6.0 fmt fixtures for aarch64 little endian and s390x big endian validate both decoders against frozen files; the generator gains a C++ writer that is skipped gracefully when regenerating fixtures for versions without fmt support. The python test harness target builds as C++20 (required by fmt tracepoints; matches the examples). New example fmt_cpp shows usage including the char*-as-string rule. Signed-off-by: Jo5ta --- examples/CMakeLists.txt | 1 + examples/fmt_cpp/CMakeLists.txt | 16 +++++++++++ examples/fmt_cpp/fmt_cpp.cpp | 27 ++++++++++++++++++ tests/decoder.tests/Meta.tests.cpp | 6 ++++ tests/golden/README.md | 2 ++ tests/golden/generator/gen.sh | 12 ++++++++ tests/golden/generator/writer_fmt.cpp | 16 +++++++++++ .../golden-1.6.0-fmt-be-s390x.clltk_trace | Bin 0 -> 4859 bytes .../golden-1.6.0-fmt-le-aarch64.clltk_trace | Bin 0 -> 4859 bytes tests/helpers/temp_target/CMakeLists.txt | 2 +- tests/test_golden.py | 25 ++++++++++++++++ tests/test_valid_build.py | 21 ++++++++++++++ 12 files changed, 127 insertions(+), 1 deletion(-) create mode 100644 examples/fmt_cpp/CMakeLists.txt create mode 100644 examples/fmt_cpp/fmt_cpp.cpp create mode 100644 tests/golden/generator/writer_fmt.cpp create mode 100644 tests/golden/golden-1.6.0-fmt-be-s390x.clltk_trace create mode 100644 tests/golden/golden-1.6.0-fmt-le-aarch64.clltk_trace diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index f1edd90..2de95ef 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -36,6 +36,7 @@ add_subdirectory(./with_libraries) add_subdirectory(./spans_c) add_subdirectory(./simple_cpp) add_subdirectory(./spans_cpp) +add_subdirectory(./fmt_cpp) add_subdirectory(./complex_cpp) add_subdirectory(./process_threads) add_subdirectory(./snapshot_cpp) diff --git a/examples/fmt_cpp/CMakeLists.txt b/examples/fmt_cpp/CMakeLists.txt new file mode 100644 index 0000000..3968f50 --- /dev/null +++ b/examples/fmt_cpp/CMakeLists.txt @@ -0,0 +1,16 @@ +# Copyright (c) 2026, International Business Machines +# SPDX-License-Identifier: BSD-2-Clause-Patent + +add_executable(example-fmt_cpp + ./fmt_cpp.cpp +) + +target_link_libraries(example-fmt_cpp +PRIVATE + clltk_tracing + clltk_example_flags +) + +add_dependencies(example-fmt_cpp + clean_up_traces +) diff --git a/examples/fmt_cpp/fmt_cpp.cpp b/examples/fmt_cpp/fmt_cpp.cpp new file mode 100644 index 0000000..130149b --- /dev/null +++ b/examples/fmt_cpp/fmt_cpp.cpp @@ -0,0 +1,27 @@ +// Copyright (c) 2026, International Business Machines +// SPDX-License-Identifier: BSD-2-Clause-Patent + +// fmt-style tracepoints (C++20): format strings use {} placeholders and are +// validated against the argument types at compile time via std::format_string. +// Same speed and mechanics as CLLTK_TRACEPOINT - only the syntax differs. + +#include "CommonLowLevelTracingKit/tracing/tracing.h" + +#include + +CLLTK_TRACEBUFFER(FMT_CPP, 4096) + +int main() +{ + CLLTK_TRACEPOINT_FMT(FMT_CPP, "loaded {} in {}ms", "module-a", 42); + CLLTK_TRACEPOINT_FMT(FMT_CPP, "hex {:x} padded {:>8} float {:.2f}", 255u, 7, 3.5); + + // a char* argument is always recorded as a string; record the pointer + // value by casting to void* + const char *name = "pointer-vs-string"; + CLLTK_TRACEPOINT_FMT(FMT_CPP, "string {} pointer {}", name, (const void *)name); + + // mixing with printf-style tracepoints in the same buffer is fine + CLLTK_TRACEPOINT(FMT_CPP, "printf style %s", "still works"); + return 0; +} diff --git a/tests/decoder.tests/Meta.tests.cpp b/tests/decoder.tests/Meta.tests.cpp index 457fe81..4c75dcd 100644 --- a/tests/decoder.tests/Meta.tests.cpp +++ b/tests/decoder.tests/Meta.tests.cpp @@ -214,6 +214,9 @@ INSTANTIATE_TEST_SUITE_P(TypeMappings, MetaEntryTypeParamTest, ::testing::Values(std::make_tuple(0, MetaEntryType::Unknown), std::make_tuple(1, MetaEntryType::Printf), std::make_tuple(2, MetaEntryType::Dump), + std::make_tuple(3, MetaEntryType::SpanBegin), + std::make_tuple(4, MetaEntryType::SpanEnd), + std::make_tuple(5, MetaEntryType::Fmt), std::make_tuple(99, MetaEntryType::Unknown), std::make_tuple(255, MetaEntryType::Unknown))); @@ -229,6 +232,9 @@ TEST_F(MetaEntryInfoTest, typeToString) { EXPECT_EQ(MetaEntryInfo::typeToString(MetaEntryType::Printf), "printf"); EXPECT_EQ(MetaEntryInfo::typeToString(MetaEntryType::Dump), "dump"); + EXPECT_EQ(MetaEntryInfo::typeToString(MetaEntryType::SpanBegin), "span_begin"); + EXPECT_EQ(MetaEntryInfo::typeToString(MetaEntryType::SpanEnd), "span_end"); + EXPECT_EQ(MetaEntryInfo::typeToString(MetaEntryType::Fmt), "fmt"); EXPECT_EQ(MetaEntryInfo::typeToString(MetaEntryType::Unknown), "unknown"); } diff --git a/tests/golden/README.md b/tests/golden/README.md index c6704c1..3f2114e 100644 --- a/tests/golden/README.md +++ b/tests/golden/README.md @@ -27,6 +27,8 @@ regenerated from source. | `golden-1.3.0-be-s390x.o` / `.so` | ELF objects compiled with library 1.3.0 headers on s390x | | `golden-1.5.0-le-aarch64.clltk_trace` | library 1.5.0 (first with span events), aarch64 | | `golden-1.5.0-be-s390x.clltk_trace` | library 1.5.0 (first with span events), s390x (big endian) | +| `golden-1.6.0-fmt-le-aarch64.clltk_trace` | library 1.6.0 (first with fmt tracepoints), aarch64 | +| `golden-1.6.0-fmt-be-s390x.clltk_trace` | library 1.6.0 (first with fmt tracepoints), s390x (big endian) | `tests/test_golden.py` decodes every fixture and asserts the formatted tracepoint messages. When the trace file format changes, add a new fixture diff --git a/tests/golden/generator/gen.sh b/tests/golden/generator/gen.sh index dc7f658..4d4ab44 100755 --- a/tests/golden/generator/gen.sh +++ b/tests/golden/generator/gen.sh @@ -20,4 +20,16 @@ gcc -std=c11 -O1 -I/src/tracing_library/include /tmp/writer.c "$LIB" -pthread -o rm -rf /tmp/traces && mkdir /tmp/traces CLLTK_TRACING_PATH=/tmp/traces /tmp/writer cp /tmp/traces/GOLDEN.clltk_trace /out/ +# fmt-style tracepoints (C++20, since 1.6.0): build the C++ writer when the +# checked-out headers and compiler support it, skip gracefully otherwise +if [ -f /src/tests/golden/generator/writer_fmt.cpp ]; then + if g++ -std=c++20 -O1 -I/src/tracing_library/include \ + /src/tests/golden/generator/writer_fmt.cpp "$LIB" -pthread -o /tmp/writer_fmt \ + 2>/tmp/writer_fmt.log; then + CLLTK_TRACING_PATH=/tmp/traces /tmp/writer_fmt + cp /tmp/traces/GOLDEN_FMT.clltk_trace /out/ + else + echo "fmt writer skipped (headers or compiler without C++20 fmt support)" + fi +fi echo "fixture written: $(ls -l /out/GOLDEN.clltk_trace)" diff --git a/tests/golden/generator/writer_fmt.cpp b/tests/golden/generator/writer_fmt.cpp new file mode 100644 index 0000000..2d609c3 --- /dev/null +++ b/tests/golden/generator/writer_fmt.cpp @@ -0,0 +1,16 @@ +// Copyright (c) 2026, International Business Machines +// SPDX-License-Identifier: BSD-2-Clause-Patent + +// Golden fixture writer for fmt-style tracepoints (C++20 only, meta type 5). +// Deterministic values; the produced GOLDEN_FMT.clltk_trace is committed. +#include "CommonLowLevelTracingKit/tracing/tracing.h" + +CLLTK_TRACEBUFFER(GOLDEN_FMT, 4096) + +int main() +{ + CLLTK_TRACEPOINT_FMT(GOLDEN_FMT, "loaded {} in {}ms", "module-a", 42); + CLLTK_TRACEPOINT_FMT(GOLDEN_FMT, "plain text no args"); + CLLTK_TRACEPOINT_FMT(GOLDEN_FMT, "hex {:x} float {:.2f}", 255u, 3.5); + return 0; +} diff --git a/tests/golden/golden-1.6.0-fmt-be-s390x.clltk_trace b/tests/golden/golden-1.6.0-fmt-be-s390x.clltk_trace new file mode 100644 index 0000000000000000000000000000000000000000..a6d51ab4ff07a5d3923a0383ed3c6f2814523a65 GIT binary patch literal 4859 zcmYdJEGnr}QMP9&N=-{EO-f||0Y)}31tct>j06ZR=mF)MLTPDte;*fDzj!y_5C&%- zpAheO*9ay?uw*k-37CQi0Zm2{oP@+Ck|_YQA6-2&R3Hysf)KxsL46-cjo@7d2ByUz zEOAi4!KkP6I8cx?H$SB`Csj8Qq(W;!9fO!Ah$ZBTqIUjYnbJB2rKMo0H7HUGl7oSw z{~dThatsW2Ak-*18UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*OqaiS)LjX3u z0i!{lA!D3_*f3zUc99BFHN8_Byu*V3lY8YkUL>8~E!9AtH!G0fVJh}y&@V1Z)-Opd zE-BVe&(BFo&C^d$%}XsxEXgm@FE7e0NiB*`%PrAME+}Bg$xlp4O;M<>RmjW(lDWkU zhwaXoec1aysR?7eY D*L#c7 literal 0 HcmV?d00001 diff --git a/tests/golden/golden-1.6.0-fmt-le-aarch64.clltk_trace b/tests/golden/golden-1.6.0-fmt-le-aarch64.clltk_trace new file mode 100644 index 0000000000000000000000000000000000000000..d02eb1f1f84d22cfdad89f413c443e2099db66ef GIT binary patch literal 4859 zcmcClR;eo~N=!~oDosmEEn;9`V`KmU3oyyRkN~AU1fdKF)h7+*y8HXMxcbGr`Gzn! z`}l--$Gb)_K}=`{tAMZ}ijmoqp!$iWJOr?-WQGdoVV5Q-TBpv?cNgrpdqA}y9J#ya zj)uTN2~Hq8H$SB`Csj9*K?}$ct`lQOauouy!0JG1#g9SMey>wvIKKu(D((bC>OV+5 z81Ojo!I-1;Xb6mkz-S1JhQMeDjE2By2#kinXb6mkz-S1JhQMeDjE2Ba3<2b^4P-W{ z;~d2L-5P31j)q8+P-x%D+f6P4+Qx2jF#X;@#?fjJ$;tyHn2J3a^oxs<^-EHVON#Z= z^K(*C^YqhG^HPfvOY)2K%ZoBgQj6l#a!d4*3kn!=@)J{1QxvLe6*BXHWNtBo=-(f^ zZ7;I<>`9Ccvs-tl8fK3dkg>iRM6&XN#YnKFASV%Md`W6Wi9%k!LSj*RF@vDewu3v^ za&{d*lo$D0wdf?w8h;=oxf(>Wf(B8TGE+$LPDW~lLbX*ztwI{mV5vY0{|PP BjAH-* literal 0 HcmV?d00001 diff --git a/tests/helpers/temp_target/CMakeLists.txt b/tests/helpers/temp_target/CMakeLists.txt index 5916842..86ba8a0 100644 --- a/tests/helpers/temp_target/CMakeLists.txt +++ b/tests/helpers/temp_target/CMakeLists.txt @@ -46,7 +46,7 @@ target_link_libraries(main_cpp set_target_properties(main_cpp PROPERTIES - CXX_STANDARD 17 + CXX_STANDARD 20 CXX_EXTENSIONS OFF CXX_STANDARD_REQUIRED ON COMPILE_WARNING_AS_ERROR ON diff --git a/tests/test_golden.py b/tests/test_golden.py index 5a75211..c136faf 100644 --- a/tests/test_golden.py +++ b/tests/test_golden.py @@ -52,6 +52,17 @@ def setUpModule(): "golden-1.3.0-be-s390x.clltk_trace", "golden-1.5.0-be-s390x.clltk_trace", ] +# fmt-style tracepoint fixtures (deterministic messages) +FMT_FIXTURES = [ + "golden-1.6.0-fmt-le-aarch64.clltk_trace", + "golden-1.6.0-fmt-be-s390x.clltk_trace", +] +EXPECTED_FMT_MESSAGES = [ + "loaded module-a in 42ms", + "plain text no args", + "hex ff float 3.50", +] + # fixtures containing span events (validated structurally: span ids are # random per generation, so exact strings differ between fixtures) SPAN_FIXTURES = [ @@ -170,6 +181,20 @@ def test_big_endian_relocatable_object(self): self._assert_meta("golden-1.3.0-be-s390x.o") +class golden_fmt_fixtures(unittest.TestCase): + def test_python_decoder(self): + for name in FMT_FIXTURES: + with self.subTest(fixture=name): + messages = decode_with_python(GOLDEN_DIR / name) + self.assertEqual(EXPECTED_FMT_MESSAGES, messages) + + def test_cli_decoder(self): + for name in FMT_FIXTURES: + with self.subTest(fixture=name): + messages = decode_with_cli(GOLDEN_DIR / name) + self.assertEqual(EXPECTED_FMT_MESSAGES, messages) + + class golden_cli_decoder(unittest.TestCase): def test_little_endian_fixtures(self): for name in LITTLE_ENDIAN_FIXTURES: diff --git a/tests/test_valid_build.py b/tests/test_valid_build.py index 623ab13..4d1b9fb 100644 --- a/tests/test_valid_build.py +++ b/tests/test_valid_build.py @@ -185,6 +185,27 @@ def test_spans(self: unittest.TestCase): sorted([begins["outer"][0], begins["inner"][0]])) pass + def test_fmt_tracepoints(self: unittest.TestCase): + """CLLTK_TRACEPOINT_FMT uses {} placeholders (C++20 only), validated + at compile time and rendered by the decoders.""" + file_content = """ + #include "CommonLowLevelTracingKit/tracing/tracing.h" + CLLTK_TRACEBUFFER(BUFFER, 4096); + int main() + { + CLLTK_TRACEPOINT_FMT(BUFFER, "loaded {} in {}ms", "module-a", 42); + CLLTK_TRACEPOINT_FMT(BUFFER, "plain text no args"); + CLLTK_TRACEPOINT_FMT(BUFFER, "hex {:x} float {:.2f}", 255u, 3.5); + return 0; + } + """ + data = process(file_content, language=Language.CPP) + formatted = data["formatted"].tolist() + self.assertIn("loaded module-a in 42ms", formatted) + self.assertIn("plain text no args", formatted) + self.assertIn("hex ff float 3.50", formatted) + pass + def test_empty(self: unittest.TestCase): for language in [Language.C, Language.CPP]: with self.subTest(language=language): From 084da763f024286d9c543332a68879bd647c0ca5 Mon Sep 17 00:00:00 2001 From: Jo5ta Date: Sat, 11 Jul 2026 01:14:26 +0200 Subject: [PATCH 4/4] docs: fmt tracepoint documentation and version 1.6.0 README usage, file specification meta type 5, technical documentation of validation and rendering, AGENTS.md gotchas. New meta type is a file layout change: minor bump. Signed-off-by: Jo5ta --- AGENTS.md | 1 + README.md | 9 +++++++++ VERSION.md | 9 ++++++++- docs/file_specification.asciidoc | 2 ++ docs/technical_documentation.asciidoc | 11 +++++++++++ 5 files changed, 31 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index bd85d96..69ec9e6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,6 +81,7 @@ docs/ AsciiDoc documentation, file format spec, diagrams. - Macros use `__VA_OPT__` (C++20/C23/GCC extension). NEVER replace with `##__VA_ARGS__` -- they are not equivalent. - `CLLTK_TRACEPOINT_DUMP` takes `(buffer, message, address, size)` -- NOT a printf format. Do not confuse with `CLLTK_TRACEPOINT`. - `CLLTK_DYN_TRACEPOINT` takes a string buffer name (not an identifier) and binds at runtime. Slower than static tracepoints. +- `CLLTK_TRACEPOINT_FMT` (meta type 5) is C++20-only; a `char*` argument is always recorded as a string ({} has no %p/%s ambiguity) - cast to `void*` for the pointer value. Format validation happens at compile time via a never-executed `_clltk_fmt_check` call. - `CLLTK_SPAN_BEGIN` is a GNU statement expression that evaluates to the new span id (`clltk_span_id_t`, a plain uint64; 0 = no parent). Span begin/end are meta entry types 3/4 and carry their ids as ordinary uint64 arguments; decoders correlate begin/end by id, not by nesting. ### ELF Sections diff --git a/README.md b/README.md index 897f771..3f5e383 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,15 @@ The name is used to associated tracepoint with this tracebuffer and is also the The tracepoints `CLLTK_TRACEPOINT` defines first the target tracebuffer, than the format string, followed by the arguments. +### fmt-style format strings (C++20) + +`CLLTK_TRACEPOINT_FMT` accepts std::format style `{}` placeholders, validated +against the argument types at compile time: + +```cpp +CLLTK_TRACEPOINT_FMT(MyFirstTracebuffer, "loaded {} in {}ms", name, duration); +``` + ### Spans Spans track scoped work with begin/end events and a carryable id: diff --git a/VERSION.md b/VERSION.md index 9198bd5..e7975dc 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1,6 +1,13 @@ -1.5.1 +1.6.0 # Change log +## 1.6.0 +- feat: fmt-style tracepoints (C++20 only). `CLLTK_TRACEPOINT_FMT(buffer, "loaded {} in {}ms", + name, ms)` uses std::format {} placeholders, validated against the argument types at compile + time via std::format_string. Argument encoding is unchanged; both decoders render the new + meta entry type 5 with their native {} formatters. A char* argument is always recorded as a + string (no %p/%s ambiguity). New meta type = file layout change, hence the minor bump. + Kernel: not applicable (C API). ## 1.5.1 - feat: `clltk export` converts trace files into Chrome/Perfetto trace event JSON (open in ui.perfetto.dev): tracepoints as instant events, spans as async begin/end diff --git a/docs/file_specification.asciidoc b/docs/file_specification.asciidoc index db71360..c7f6fb3 100644 --- a/docs/file_specification.asciidoc +++ b/docs/file_specification.asciidoc @@ -232,6 +232,8 @@ The meta_entry_type differentiates the tracepoint styles: * 3 = span begin (the string is the span name; two uint64 arguments: span id, parent span id or 0) * 4 = span end (empty string; one uint64 argument: span id) +* 5 = fmt tracepoint (format string with C++20 std::format {} placeholders; + argument encoding identical to printf tracepoints) Decoders skip entries with unknown types, so the list can grow without breaking old decoders. diff --git a/docs/technical_documentation.asciidoc b/docs/technical_documentation.asciidoc index 03cd9fb..f6d9b69 100644 --- a/docs/technical_documentation.asciidoc +++ b/docs/technical_documentation.asciidoc @@ -177,6 +177,17 @@ rendering and correlation differ by meta type. Span ids come from 32 bit counter, unique without cross-process coordination, never 0 (0 means "no parent"). +=== fmt-style tracepoints +`CLLTK_TRACEPOINT_FMT` (meta entry type 5) stores the {} format string +verbatim and encodes arguments exactly like printf tracepoints - the library +types arguments independently of the format syntax. Compile-time validation +happens through a never-executed call to a helper taking +`std::format_string`, whose consteval constructor checks the literal +against the argument types. The runtime call goes through an entry point +without the printf format attribute, since {} strings must not be checked as +printf formats. Decoders render type 5 with their native {} formatters +(python `str.format`, C++ `std::vformat`). + === caching process and thread id Each call to `syscall(SYS_getpid)` and `syscall(SYS_gettid)` requires a context switch to kernel and therefore burdens a heavy overhead. To remove this context switch, pid (process id) and tid (thread id) should be cached. + It's possible to detect if a code is executed inside a new thread with storage specifier `__thread` for tid and detect forking with `pthread_atfork`.