CppInterOp
C++ Language Interoperability Layer
Loading...
Searching...
No Matches
Tracing.h
Go to the documentation of this file.
1//===--- Tracing.h - A layer for tracing and reproducibility ----*- C++ -*-===//
2//
3// Part of the compiler-research project, under the Apache License v2.0 with
4// LLVM Exceptions.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// This file defines API for performance analysis and building reproducers.
10//
11//===----------------------------------------------------------------------===//
12
13#ifndef CPPINTEROP_TRACING_H
14#define CPPINTEROP_TRACING_H
15
16// Visibility for tracing symbols that must be exported from the shared
17// library so that tests (and the crash handler) can access them.
18#if defined(_WIN32) || defined(__CYGWIN__)
19#define CPPINTEROP_TRACE_API __declspec(dllexport)
20#elif defined(__GNUC__)
21#define CPPINTEROP_TRACE_API __attribute__((__visibility__("default")))
22#else
23#define CPPINTEROP_TRACE_API
24#endif
25
27
28#include "llvm/ADT/SmallString.h"
29#include "llvm/ADT/SmallVector.h"
30#include "llvm/ADT/StringMap.h"
31#include "llvm/Support/FormatVariadic.h"
32#include "llvm/Support/Timer.h"
33#include "llvm/Support/raw_ostream.h"
34
35#include <cassert>
36#include <cstdint>
37#include <functional>
38#include <memory>
39#include <optional>
40#include <string>
41#include <string_view>
42#include <type_traits>
43#include <unordered_map>
44#include <vector>
45
46namespace CppInterOp {
47namespace Tracing {
48
49class TraceInfo;
50
51/// Process-global tracer pointer. Exported because TracingTests and
52/// the crash handler (linked-mode consumers) read it directly; cppyy
53/// and Dispatch.h consumers go through the DispatchRaw trace slots
54/// declared in CppInterOpTypes.h instead.
55extern CPPINTEROP_TRACE_API TraceInfo* TheTraceInfo;
56
57class TraceInfo {
58 llvm::TimerGroup m_TG;
59 llvm::StringMap<std::unique_ptr<llvm::Timer>> m_Timers;
60 std::vector<llvm::Timer*> m_TimerStack;
61
62 std::unordered_map<const void*, std::string> m_HandleMap;
63 unsigned m_VarCount = 0;
64 /// Monotonic suffix for `_retN` placeholders -- one slot per call
65 /// that returns std::vector<P*>. Separate from m_VarCount so the
66 /// vN handle namespace stays dense.
67 unsigned m_RetCount = 0;
68 /// Monotonic suffix for `_outN` -- one slot per distinct OUT
69 /// pointer-container source. Multiple calls passing the same
70 /// container alias the same slot (see m_OutAliases).
71 unsigned m_OutCount = 0;
72 /// Source address of an OUT container -> its `_outN` index. Lets
73 /// the reproducer faithfully replay the API's append-on-call
74 /// contract: a vector reused across calls keeps its single buffer.
75 std::unordered_map<const void*, unsigned> m_OutAliases;
76
77 std::vector<std::string> m_Log;
78 size_t m_RegionStart = 0; ///< Log index where current region began.
79 bool m_InRegion = false; ///< True between StartTracing/StopTracing.
80 /// Set while the reproducer is being emitted. Gates appendToLog and
81 /// setLogEntry so the dumper's own INTEROP_TRACE-wrapped calls
82 /// (GetVersion, GetBuildInfo) don't recurse into m_Log.
83 bool m_Dumping = false;
84
85public:
86 TraceInfo() : m_TG("CppInterOp", "CppInterOp Timing Report") {}
87 ~TraceInfo() { TheTraceInfo = nullptr; }
88 TraceInfo(const TraceInfo&) = delete;
89 TraceInfo& operator=(const TraceInfo&) = delete;
90 TraceInfo(TraceInfo&&) = delete;
92
93 static bool isEnabled() { return TheTraceInfo; }
94
95 llvm::Timer& getTimer(llvm::StringRef Name) {
96 auto& T = m_Timers[Name];
97 if (!T)
98 T = std::make_unique<llvm::Timer>(Name, Name, m_TG);
99 return *T;
100 }
101
102 /// True when at least one TraceRegion is currently active.
103 bool insideTracedRegion() const { return !m_TimerStack.empty(); }
104
105 void pushTimer(llvm::Timer* T) {
106 if (!m_TimerStack.empty())
107 m_TimerStack.back()->stopTimer();
108 m_TimerStack.push_back(T);
109 T->startTimer();
110 }
111
112 void popTimer() {
113 if (m_TimerStack.empty())
114 return;
115 m_TimerStack.back()->stopTimer();
116 m_TimerStack.pop_back();
117 if (!m_TimerStack.empty())
118 m_TimerStack.back()->startTimer();
119 }
120
121 std::string getOrRegisterHandle(const void* p) {
122 if (!p)
123 return "";
124 auto it = m_HandleMap.find(p);
125 if (it != m_HandleMap.end())
126 return it->second;
127 return m_HandleMap[p] = "v" + std::to_string(++m_VarCount);
128 }
129
130 /// Resolve a pointer to a printable form.
131 /// - "nullptr" for an actual null pointer.
132 /// - "vN" for a pointer registered via getOrRegisterHandle.
133 /// - "" (empty) for a non-null pointer never seen by the tracer --
134 /// the caller decides how to render the unknown case (e.g.
135 /// `nullptr /*unknown*/` in argument lists, "is this new?" in the
136 /// producer-side `auto vN = ...` gating logic).
137 std::string lookupHandle(const void* p) {
138 if (!p)
139 return "nullptr";
140 auto it = m_HandleMap.find(p);
141 return (it != m_HandleMap.end()) ? it->second : "";
142 }
143
144 /// Allocate the next `_retN` index for a vector-return placeholder.
145 unsigned nextRetIndex() { return m_RetCount++; }
146
147 /// Resolve an OUT-container source address to its `_outN` index.
148 /// First call with a given address allocates a fresh slot; later
149 /// calls return the same slot so the reproducer reuses the buffer.
150 /// \pre \p Addr is non-null (MakeOutParam captures `&C`).
151 /// \returns {idx, true} on first use, {idx, false} on alias.
152 std::pair<unsigned, bool> outIndexFor(const void* Addr) {
153 assert(Addr && "OutParam without a source address");
154 auto it = m_OutAliases.find(Addr);
155 if (it != m_OutAliases.end())
156 return {it->second, false};
157 unsigned idx = m_OutCount++;
158 m_OutAliases.emplace(Addr, idx);
159 return {idx, true};
160 }
161
162 /// Append a line; the returned index pairs with setLogEntry to
163 /// rewrite the same slot later (TraceRegion's placeholder pattern).
164 size_t appendToLog(const std::string& line) {
165 if (m_Dumping)
166 return 0;
167 size_t idx = m_Log.size();
168 m_Log.push_back(line);
169 if (m_InRegion && m_WriteOnStdErr)
170 llvm::errs() << line << "\n";
171 return idx;
172 }
173 void setLogEntry(size_t idx, const std::string& line) {
174 if (m_Dumping)
175 return;
176 m_Log[idx] = line;
177 }
178 void setDumping(bool v) { m_Dumping = v; }
179 const std::vector<std::string>& getLog() const { return m_Log; }
180 std::string getLastLogEntry() const {
181 return m_Log.empty() ? "" : m_Log.back();
182 }
183
184 /// Write the accumulated reproducer log to a file.
185 /// \param Version optional version string embedded as comments.
186 CPPINTEROP_TRACE_API std::string writeToFile(const std::string& Version = "");
187
188 /// Begin a traced region. Returns the path where StopTracing will write.
189 /// \param WriteOnStdErr if true, also emit the reproducer to stderr.
190 CPPINTEROP_TRACE_API std::string StartRegion(bool WriteOnStdErr = true);
191
192 /// End the traced region and write only the region's entries to the file.
193 CPPINTEROP_TRACE_API void StopRegion(const std::string& Version = "");
194
195private:
196 std::string m_RegionPath;
197 bool m_WriteOnStdErr = false;
198
199public:
200 void clear() {
201 // Stop any running timers before clearing to avoid triggering
202 // TimerGroup's destructor report.
203 while (!m_TimerStack.empty()) {
204 m_TimerStack.back()->stopTimer();
205 m_TimerStack.pop_back();
206 }
207 // Clear timers after clearing the group to suppress the report.
208 m_TG.clear();
209 m_Timers.clear();
210 m_HandleMap.clear();
211 m_Log.clear();
212 m_VarCount = 0;
213 m_RetCount = 0;
214 m_OutCount = 0;
215 m_OutAliases.clear();
216 }
217};
218
219/// Activate tracing. Called once during process initialization.
220/// After this, TheTraceInfo is non-null and all INTEROP_TRACE calls record.
222
223/// Begin recording a traced region. If tracing is not yet active, activates
224/// it. Returns the path where StopTracing() will write the reproducer.
225/// \param WriteOnStdErr if true, also emit the reproducer to stderr on stop.
226inline std::string StartTracing(bool WriteOnStdErr = true) {
227 if (!TheTraceInfo)
228 InitTracing();
229 return TheTraceInfo->StartRegion(WriteOnStdErr);
230}
231
232/// End the traced region and write the reproducer file containing only the
233/// calls made between StartTracing() and this call.
234inline void StopTracing(const std::string& Version = "") {
235 if (TheTraceInfo)
236 TheTraceInfo->StopRegion(Version);
237}
238
239/// Marks a function parameter as an output container (e.g. std::vector<T>&
240/// that the function fills). Constructed via the INTEROP_OUT(var) macro.
241///
242/// Purpose:
243/// - Excluded from the reproducer's argument list (it's an output, not input).
244/// - If the container holds pointers, its elements are registered as handles
245/// at trace-region exit so later calls can reference them by name.
246///
247/// The container type is erased at construction via MakeOutParam() so that
248/// all downstream code (ReproBuffer, TraceRegion) works with a single
249/// concrete type — no template specializations or detection traits needed.
250struct OutParam {
251 /// Register the container's pointer elements as handles and emit
252 /// one `void* vN = _outN[i] : nullptr;` decl per newly-registered
253 /// element so a later call referencing the registered name finds a
254 /// real binding.
255 std::function<void(TraceInfo&, unsigned OutIdx)> RegisterHandles;
256 /// Drives the `_outN` preamble + alias path. Non-pointer containers
257 /// stay on the legacy "skip in arg list" rendering -- their type is
258 /// not erasable to void*, so the preamble decl would not type-check.
259 bool IsPointerContainer = false;
260 /// Scalar pointer OUT (e.g. `bool*`); rendered as `nullptr`.
261 bool IsScalarPointer = false;
262 /// Address of the source container object (not its data buffer);
263 /// multiple calls with the same container alias the same `_outN`
264 /// buffer in the reproducer so accumulation across calls replays
265 /// faithfully. Stable across capacity growth -- a `push_back` that
266 /// reallocates the heap buffer leaves the object's address put.
267 const void* SourceAddr = nullptr;
268};
269
270/// Create an OutParam for any container. Only sets up handle registration
271/// when the container's value_type is a pointer.
272template <typename Container> OutParam MakeOutParam(const Container& C) {
273 OutParam OP;
274 OP.SourceAddr = static_cast<const void*>(&C);
275 using Value = typename Container::value_type;
276 if constexpr (std::is_pointer_v<Value>) {
277 OP.IsPointerContainer = true;
278 OP.RegisterHandles = [&C](TraceInfo& TI, unsigned OutIdx) {
279 size_t I = 0;
280 for (const auto& Elem : C) {
281 const void* P = static_cast<const void*>(Elem);
282 bool isNew = TI.lookupHandle(P).empty();
283 std::string Name = TI.getOrRegisterHandle(P);
284 // Bounds-guard with `.size() > i` for replays where the
285 // function fills fewer elements than the original call did.
286 if (isNew && P)
287 TI.appendToLog(llvm::formatv(" void* {0} = _out{1}.size() > {2} ? "
288 "_out{1}[{2}] : nullptr;",
289 Name, OutIdx, I)
290 .str());
291 ++I;
292 }
293 };
294 }
295 return OP;
296}
297
298/// Scalar pointer overload (e.g. `bool* HadError`). Rendered as
299/// `nullptr` at the call site -- replay only needs the call sequence.
300template <typename T> OutParam MakeOutParam(T*) {
301 OutParam OP;
302 OP.IsScalarPointer = true;
303 return OP;
304}
305
306/// Internal helper to stringify arguments into a C++ call format.
308 llvm::SmallString<128> Buffer;
309 llvm::raw_svector_ostream OS;
310
312
313 // Opaque handle structs — unwrap .data and resolve to registered name.
314 void append(Cpp::DeclRef h) { append(h.data); }
315 void append(Cpp::TypeRef h) { append(h.data); }
316 void append(Cpp::FuncRef h) { append(h.data); }
317 void append(Cpp::ObjectRef h) { append(h.data); }
318 void append(Cpp::InterpRef h) { append(h.data); }
319 void append(Cpp::ConstDeclRef h) { append(h.data); }
320 void append(Cpp::ConstTypeRef h) { append(h.data); }
321 void append(Cpp::ConstFuncRef h) { append(h.data); }
322
323 // Raw void* pointers — resolved to their registered name.
324 void append(const void* p) {
325 if (!p) {
326 OS << "nullptr";
327 return;
328 }
329 auto h = TheTraceInfo->lookupHandle(p);
330 if (h.empty())
331 OS << "nullptr /*unknown*/";
332 else
333 OS << h;
334 }
335
336 // Strings -- emit a plain `"..."` literal when the content is
337 // printable ASCII without `"`, `\`, or control chars; otherwise
338 // fall back to `R"CPPI(...)CPPI"`, which passes the bytes through
339 // verbatim. The raw form is reserved for the cases that actually
340 // need it (quotes, backslashes, newlines from Cpp::Process /
341 // Cpp::Declare source blocks); plain literals keep the trace lines
342 // short and human-readable for the common case (identifiers,
343 // paths, simple expressions).
344 void appendRaw(std::string_view s) {
345 bool NeedsRaw = false;
346 for (char c : s) {
347 auto u = static_cast<unsigned char>(c);
348 // 0x20 is space: anything below it is a C0 control char (incl. \n,
349 // \t, \r) that would break a plain quoted literal; 0x7f is DEL.
350 if (c == '"' || c == '\\' || u < 0x20 || u == 0x7f) {
351 NeedsRaw = true;
352 break;
353 }
354 }
355 if (NeedsRaw)
356 OS << "R\"CPPI(" << s << ")CPPI\"";
357 else
358 OS << '"' << s << '"';
359 }
360 void append(const char* s) {
361 appendRaw(s ? std::string_view(s) : std::string_view());
362 }
363 void append(const std::string& s) { appendRaw(s); }
364
365 // Numeric types — printed directly.
366 void append(bool v) { OS << (v ? "true" : "false"); }
367 void append(int v) { OS << v; }
368 void append(unsigned v) { OS << v; }
369 void append(long v) { OS << v; }
370 void append(unsigned long v) { OS << v; }
371 void append(long long v) { OS << v; }
372 void append(unsigned long long v) { OS << v; }
373 void append(double d) { OS << llvm::formatv("{0:f}", d); }
374 void append(float f) { OS << llvm::formatv("{0:f}", f); }
375
376 // Vector input args -- emit a braced init list of recursively-formatted
377 // elements so the reproducer's call-site literal compiles.
378 template <typename T> void append(const std::vector<T>& V) {
379 OS << "{";
380 bool First = true;
381 for (const auto& E : V) {
382 if (!First)
383 OS << ", ";
384 First = false;
385 append(E);
386 }
387 OS << "}";
388 }
389
390 // TemplateArgInfo: brace-init through the (DeclRef, const char*)
391 // ctor so the reproducer compiles. m_Type takes the void* path
392 // (renders as vN); nullptr m_IntegralValue must render as `nullptr`
393 // (the ctor's default), not the empty string the const char* path
394 // would produce.
395 void append(const Cpp::TemplateArgInfo& tai) {
396 OS << "Cpp::TemplateArgInfo{";
397 append(static_cast<const void*>(tai.m_Type));
398 OS << ", ";
399 if (tai.m_IntegralValue == nullptr)
400 OS << "nullptr";
401 else
402 appendRaw(tai.m_IntegralValue);
403 OS << "}";
404 }
405
406 // Enums: emit "static_cast<EnumName>(N)" so the reproducer compiles.
407 // EnumName comes from the compiler's pretty signature -- no RTTI.
408 // Compute outside any lambda: gcc/clang substitute T into a lambda's
409 // signature, which drops the "T = " marker we parse from.
410 static std::string parseEnumName(const char* sig) {
411 llvm::StringRef s(sig);
412#ifdef _MSC_VER
413 // MSVC: "... ReproBuffer::append<enum QualKind>(enum QualKind)"
414 auto pos = s.find("append<");
415 if (pos == llvm::StringRef::npos)
416 return {};
417 s = s.drop_front(pos + 7);
418 for (auto kw : {"enum ", "class ", "struct "})
419 if (s.consume_front(kw))
420 break;
421 return s.take_until([](char c) { return c == '>'; }).str();
422#else
423 // gcc: "... [with T = QualKind; ...]"
424 // clang: "... [T = QualKind]"
425 auto pos = s.find("T = ");
426 if (pos == llvm::StringRef::npos)
427 return {};
428 return s.drop_front(pos + 4)
429 .take_until([](char c) { return c == ',' || c == ';' || c == ']'; })
430 .str();
431#endif
432 }
433 template <typename T, std::enable_if_t<std::is_enum_v<T>, int> = 0>
434 void append(T v) {
435#ifdef _MSC_VER
436 static const std::string TN = parseEnumName(__FUNCSIG__);
437#else
438 static const std::string TN = parseEnumName(__PRETTY_FUNCTION__);
439#endif
440 OS << "static_cast<" << TN << ">("
441 << +static_cast<std::underlying_type_t<T>>(v) << ")";
442 }
443 // Catch-all for non-enum, non-pointer types.
444 template <
445 typename T,
446 std::enable_if_t<!std::is_enum_v<T> && !std::is_pointer_v<T>, int> = 0>
447 void append(const T&) {
448 OS << "?";
449 }
450
451 /// Format a comma-separated argument list. Pointer-container OUTs
452 /// emit `_outN` (next index from \p OutIndices); scalar pointer OUTs
453 /// emit `nullptr` (the replay does not consume the value); non-pointer
454 /// containers are skipped, matching the legacy rendering.
455 template <typename... Args>
456 void format(llvm::ArrayRef<unsigned> OutIndices, Args&&... args) {
457 bool first = true;
458 size_t nextOut = 0;
459 auto appendOne = [&](auto&& val) {
460 using V = std::decay_t<decltype(val)>;
461 if constexpr (std::is_same_v<V, OutParam>) {
462 if (val.IsPointerContainer) {
463 if (!first)
464 OS << ", ";
465 first = false;
466 OS << "_out" << OutIndices[nextOut++];
467 } else if (val.IsScalarPointer) {
468 if (!first)
469 OS << ", ";
470 first = false;
471 OS << "nullptr";
472 }
473 } else {
474 if (!first)
475 OS << ", ";
476 first = false;
477 append(std::forward<decltype(val)>(val));
478 }
479 };
480 (appendOne(std::forward<Args>(args)), ...);
481 }
482};
483
484/// Matches std::vector<T*> for any pointer element type. Used by
485/// TraceRegion::record to recognise functions whose return value is a
486/// vector of opaque handles (e.g. GetFunctionsUsingName), so the
487/// reproducer can name the vector and read its elements out by index.
488template <typename T> struct is_pointer_vector : std::false_type {};
489template <typename T>
490struct is_pointer_vector<std::vector<T*>> : std::true_type {};
491template <typename T>
493
494/// Detect opaque handle structs (Cpp::DeclRef, Cpp::TypeRef, etc.)
495template <typename T>
496inline constexpr bool is_handle_v =
497 std::is_same_v<T, Cpp::DeclRef> || std::is_same_v<T, Cpp::TypeRef> ||
498 std::is_same_v<T, Cpp::FuncRef> || std::is_same_v<T, Cpp::ObjectRef> ||
499 std::is_same_v<T, Cpp::InterpRef> || std::is_same_v<T, Cpp::ConstDeclRef> ||
500 std::is_same_v<T, Cpp::ConstTypeRef> ||
501 std::is_same_v<T, Cpp::ConstFuncRef>;
502
503/// Detect vector-of-handle types.
504template <typename T> struct is_handle_vector : std::false_type {};
505template <>
506struct is_handle_vector<std::vector<Cpp::DeclRef>> : std::true_type {};
507template <>
508struct is_handle_vector<std::vector<Cpp::FuncRef>> : std::true_type {};
509template <typename T>
511
512/// Holds all the data that is only needed when tracing is active.
513/// Heap-allocated only when TheTraceInfo != nullptr, so disabled tracing
514/// pays zero cost beyond a single pointer + bool on the stack.
515struct TraceData {
516 const char* Name;
517 llvm::SmallString<128> ArgStr;
518 void* Result = nullptr;
519 bool HasPtrResult = false;
520 /// Set when the call returned std::vector<P*>; ~TraceRegion uses this
521 /// to spell `auto _retN = Cpp::Foo(...)` so element decls can read
522 /// `_retN[i]` and the replay sees the original pointers.
523 bool HasRetVec = false;
524 /// Snapshot of the returned vector's pointer elements, taken at
525 /// record() time -- the source vector goes out of scope before
526 /// ~TraceRegion runs.
527 llvm::SmallVector<const void*, 8> RetVecPtrs;
528 double StartTime = 0;
529 bool Returned = false;
530 llvm::SmallVector<std::function<void(TraceInfo&, unsigned)>, 2> OutCallbacks;
531 /// `_outN` indices for this call's OUT pointer-containers, in
532 /// left-to-right order. Consumed by format() at the call site and
533 /// by ~TraceRegion to emit the preamble decls.
534 llvm::SmallVector<unsigned, 2> OutIndices;
535 /// Per-OUT argument flag: true on the first call with this source
536 /// container (preamble decl needed), false on later calls reusing
537 /// the same slot.
538 llvm::SmallVector<bool, 2> OutFirstUse;
539 /// Set when another TraceRegion was already active at construction.
540 /// Nested calls skip log emission but still register handles.
541 bool Nested = false;
542 /// Slot for the ctor-emitted placeholder; the dtor rewrites it.
543 size_t LogIndex = 0;
544};
545
547 std::unique_ptr<TraceData> m_Data;
548
549 /// Resolve the OUT pointer-container's `_outN` index up-front so
550 /// format() can render the call site's alias and the dtor knows
551 /// whether to emit the preamble decl (only on first use of a given
552 /// source container). Skipped for nested calls (no emission); the
553 /// handle callback still queues so the outer call can resolve names.
554 void captureArg(OutParam&& op) {
555 if (!m_Data->Nested && op.IsPointerContainer) {
556 auto [idx, firstUse] = TheTraceInfo->outIndexFor(op.SourceAddr);
557 m_Data->OutIndices.push_back(idx);
558 m_Data->OutFirstUse.push_back(firstUse);
559 }
560 if (op.RegisterHandles)
561 m_Data->OutCallbacks.push_back(std::move(op.RegisterHandles));
562 }
563 template <typename T> void captureArg(T&&) {}
564
565public:
566 template <typename... Args> TraceRegion(const char* Name, Args&&... args) {
567 if (!TheTraceInfo)
568 return;
569 m_Data = std::make_unique<TraceData>();
570 m_Data->Name = Name;
571 TraceInfo& TI = *TheTraceInfo;
572 // Detect nesting before pushing this call's frame.
573 m_Data->Nested = TI.insideTracedRegion();
574 // captureArg before format(): it fills OutIndices that format()
575 // consumes.
576 (captureArg(std::forward<Args>(args)), ...);
577 if (!m_Data->Nested) {
578 if constexpr (sizeof...(args) > 0) {
579 ReproBuffer RB;
580 RB.format(m_Data->OutIndices, std::forward<Args>(args)...);
581 m_Data->ArgStr = RB.Buffer;
582 }
583 // `_outN` preamble + placeholder so an abort before the dtor
584 // still leaves a record of the failing call. Emit the preamble
585 // only on first use of each source container; reused ones alias
586 // the existing slot.
587 for (size_t i = 0; i < m_Data->OutIndices.size(); ++i)
588 if (m_Data->OutFirstUse[i])
589 TI.appendToLog(llvm::formatv(" std::vector<void*> _out{0};",
590 m_Data->OutIndices[i])
591 .str());
592 m_Data->LogIndex = TI.appendToLog(
593 llvm::formatv(" Cpp::{0}({1}); // [aborted before return]",
594 m_Data->Name, m_Data->ArgStr)
595 .str());
596 }
597 TI.pushTimer(&TI.getTimer(Name));
598 m_Data->StartTime = llvm::TimeRecord::getCurrentTime(false).getWallTime();
599 }
600
602 if (!m_Data)
603 return;
604
605 if (!m_Data->Returned) {
606 llvm::errs() << "ERROR: Function '" << m_Data->Name
607 << "' exited without calling INTEROP_RETURN!\n";
608 assert(
609 m_Data->Returned &&
610 "Unannotated exit branch detected: use `return INTEROP_RETURN(...)`");
611 }
612
613 auto EndTime = llvm::TimeRecord::getCurrentTime(false).getWallTime();
614 auto Dur = static_cast<long long>((EndTime - m_Data->StartTime) * 1e9);
615 TraceInfo& TI = *TheTraceInfo;
616 TI.popTimer();
617
618 // Nested calls don't reach the log -- their args reference
619 // outer-scope locals the reproducer doesn't have.
620 if (m_Data->Nested) {
621 m_Data.reset();
622 return;
623 }
624
625 // Allocate a `_retN` slot up-front for vector-of-pointer returns so
626 // the call line spells `auto _retN = ...` and the per-element decls
627 // below can read `_retN[i]` (the replay sees the original
628 // pointers, not literal nulls).
629 int RetIdx = -1;
630 llvm::SmallVector<std::pair<size_t, std::string>, 8> RetDecls;
631 if (m_Data->HasRetVec) {
632 RetIdx = static_cast<int>(TI.nextRetIndex());
633 for (size_t i = 0; i < m_Data->RetVecPtrs.size(); ++i) {
634 const void* p = m_Data->RetVecPtrs[i];
635 if (!p)
636 continue;
637 bool isNew = TI.lookupHandle(p).empty();
638 std::string Name = TI.getOrRegisterHandle(p);
639 if (isNew)
640 RetDecls.emplace_back(i, std::move(Name));
641 }
642 }
643
644 std::string VarPart;
645 if (m_Data->Result) {
646 bool isNew = TI.lookupHandle(m_Data->Result).empty();
647 std::string HandleName = TI.getOrRegisterHandle(m_Data->Result);
648 VarPart =
649 llvm::formatv(isNew ? "auto {0} = " : "/*{0}*/ ", HandleName).str();
650 } else if (m_Data->HasPtrResult) {
651 VarPart = "/*nullptr*/ ";
652 } else if (RetIdx >= 0) {
653 VarPart = llvm::formatv("auto _ret{0} = ", RetIdx).str();
654 }
655
656 // Rewrite the placeholder with the completed call line.
657 std::string Call = llvm::formatv(" {0}Cpp::{1}({2}); // [{3} ns]", VarPart,
658 m_Data->Name, m_Data->ArgStr, Dur);
659 TI.setLogEntry(m_Data->LogIndex, Call);
660
661 // After the call: register OUT-element handles and emit one
662 // `void* vN = _outN[i] : nullptr;` decl per newly-seen element so a
663 // later call referencing the registered name finds a real binding.
664 for (size_t i = 0; i < m_Data->OutCallbacks.size(); ++i)
665 m_Data->OutCallbacks[i](TI, m_Data->OutIndices[i]);
666
667 // Per-element extractions for vector returns. Bounds-guard so the
668 // line is safe even if the replay produces a shorter vector than
669 // the original.
670 for (auto& [Idx, Name] : RetDecls)
671 TI.appendToLog(
672 llvm::formatv(
673 " void* {0} = _ret{1}.size() > {2} ? _ret{1}[{2}] : nullptr;",
674 Name, RetIdx, Idx)
675 .str());
676
677 m_Data.reset();
678 }
679
680 TraceRegion(const TraceRegion&) = delete;
682 TraceRegion(TraceRegion&&) noexcept = default;
683 TraceRegion& operator=(TraceRegion&&) = delete;
684
685 [[nodiscard]] bool isActive() const { return m_Data != nullptr; }
686
687 /// Record a non-void return value. Tracks pointer results for the
688 /// reproducer's handle chain (e.g. auto v1 = Cpp::GetScope(...)).
689 template <typename T> T record(T val) {
690 if (!m_Data)
691 return val;
692 m_Data->Returned = true;
693 if constexpr (is_handle_v<T>) {
694 m_Data->HasPtrResult = true;
695 m_Data->Result = const_cast<void*>(static_cast<const void*>(val.data));
696 } else if constexpr (std::is_pointer_v<T>) {
697 m_Data->HasPtrResult = true;
698 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-const-cast)
699 m_Data->Result = const_cast<void*>(static_cast<const void*>(val));
700 } else if constexpr (std::is_null_pointer_v<T>) {
701 m_Data->HasPtrResult = true;
702 } else if constexpr (is_handle_vector_v<T>) {
703 m_Data->HasRetVec = true;
704 for (const auto& h : val)
705 m_Data->RetVecPtrs.push_back(h.data);
706 } else if constexpr (is_pointer_vector_v<T>) {
707 // Snapshot the element pointers; the source vector is owned by
708 // the API call site and goes out of scope before ~TraceRegion.
709 m_Data->HasRetVec = true;
710 for (auto* p : val)
711 m_Data->RetVecPtrs.push_back(static_cast<const void*>(p));
712 }
713 return val;
714 }
715
716 void recordVoid() {
717 if (m_Data)
718 m_Data->Returned = true;
719 }
720
721 /// Count parameters from a __PRETTY_FUNCTION__ / __FUNCSIG__ string.
722 /// Counts top-level commas between the outermost '(' and ')' of the
723 /// function signature, handling nested <>, (), and [] correctly.
724 static unsigned countParams(const char* sig) {
725 // Find the first '(' — that's the start of the parameter list.
726 const char* p = sig;
727 while (*p && *p != '(')
728 ++p;
729 if (!*p)
730 return 0;
731 ++p; // skip '('
732
733 // Skip whitespace.
734 while (*p == ' ')
735 ++p;
736 if (*p == ')')
737 return 0; // empty parameter list
738
739 // MSVC's __FUNCSIG__ uses (void) for no-parameter functions.
740 if (p[0] == 'v' && p[1] == 'o' && p[2] == 'i' && p[3] == 'd' &&
741 (p[4] == ')' || (p[4] == ' ' && p[5] == ')')))
742 return 0;
743
744 unsigned count = 1; // at least one parameter
745 int depth = 0; // nesting depth for <>, (), []
746 for (; *p; ++p) {
747 switch (*p) {
748 case '<':
749 case '(':
750 case '[':
751 ++depth;
752 break;
753 case '>':
754 case ')':
755 case ']':
756 if (depth > 0)
757 --depth;
758 else
759 return count; // closing ')' of the parameter list
760 break;
761 case ',':
762 if (depth == 0)
763 ++count;
764 break;
765 default:
766 break;
767 }
768 }
769 return count;
770 }
771
772 /// Bit i set iff Args[i] is OutParam (i.e. wrapped in INTEROP_OUT).
773 /// The trailing `false` keeps the array non-empty for sizeof...(Args)==0.
774 template <typename... Args> static constexpr uint64_t computeOutMask() {
775 constexpr bool isOut[] = {std::is_same_v<std::decay_t<Args>, OutParam>...,
776 false};
777 uint64_t mask = 0;
778 for (std::size_t i = 0; i < sizeof...(Args); ++i)
779 if (isOut[i])
780 mask |= 1ULL << i;
781 return mask;
782 }
783
784 /// Helper to allow INTEROP_TRACE() to work with zero or more arguments
785 /// without relying on non-standard ##__VA_ARGS__ comma elision.
786 struct Proxy {
787 const char* Name;
788 const char* Sig;
789 template <typename... Args> TraceRegion operator()(Args&&... args) {
790#ifndef NDEBUG
791 unsigned expected = countParams(Sig);
792 unsigned actual = sizeof...(Args);
793 if (expected != actual) {
794 llvm::errs() << "ERROR: INTEROP_TRACE argument count mismatch in '"
795 << Name << "': function has " << expected
796 << " parameter(s) but INTEROP_TRACE received " << actual
797 << " argument(s).\n"
798 << " Signature: " << Sig << "\n";
799 assert(expected == actual &&
800 "INTEROP_TRACE argument count does not match function "
801 "parameters. Update the INTEROP_TRACE call.");
802 }
803#endif
804 // OUT-mask check runs in any build but only when tracing is active,
805 // so the per-call cost off-trace is one load + branch. The diagnostic
806 // is unconditional; assert() is a no-op in NDEBUG, so Release reports
807 // the drift via stderr without aborting.
808 if (TheTraceInfo) {
809 if (auto Expected = lookupOutMask(Name)) {
810 uint64_t actual_out = computeOutMask<Args...>();
811 if (*Expected != actual_out) {
812 llvm::errs() << formatOutMaskMismatchMessage(Name, *Expected,
813 actual_out);
814 assert(
815 *Expected == actual_out &&
816 "INTEROP_TRACE OUT-arg coverage does not match the .td "
817 "OutArg<...> declarations. Wrap or unwrap with INTEROP_OUT.");
818 }
819 }
820 }
821 return TraceRegion(Name, std::forward<Args>(args)...);
822 }
823 };
824
825 /// .td-declared OUT-arg bitmask for \p Name (matches CppName /
826 /// __func__), or std::nullopt for non-public-API tracepoints.
827 CPPINTEROP_TRACE_API static std::optional<uint64_t>
828 lookupOutMask(llvm::StringRef Name);
829
830 /// Pure-function diagnostic for the OUT-mask mismatch. Extracted so a
831 /// non-death test can cover the format path -- the assert path itself
832 /// runs in a forked child whose coverage data is not merged back.
833 static std::string formatOutMaskMismatchMessage(llvm::StringRef Name,
834 uint64_t expected,
835 uint64_t actual) {
836 return llvm::formatv("ERROR: INTEROP_OUT coverage mismatch in '{0}': .td "
837 "OutArg mask {1:X} vs INTEROP_OUT-wrapped args "
838 "{2:X}.\n",
839 Name, expected, actual)
840 .str();
841 }
842};
843
844} // namespace Tracing
845} // namespace CppInterOp
846
847#ifdef _MSC_VER
848#define INTEROP_FUNC_SIG __FUNCSIG__
849#else
850#define INTEROP_FUNC_SIG __PRETTY_FUNCTION__
851#endif
852
853#define INTEROP_TRACE(...) \
854 CppInterOp::Tracing::TraceRegion _TR = \
855 CppInterOp::Tracing::TraceRegion::Proxy{__func__, \
856 INTEROP_FUNC_SIG}(__VA_ARGS__)
857
858#define INTEROP_RETURN(Val) _TR.record(Val)
859#define INTEROP_VOID_RETURN() (_TR.recordVoid())
860
861#define INTEROP_OUT(Var) CppInterOp::Tracing::MakeOutParam(Var)
862
863#endif // CPPINTEROP_TRACING_H
#define CPPINTEROP_TRACE_API
Definition Tracing.h:23
TraceInfo(const TraceInfo &)=delete
std::string lookupHandle(const void *p)
Resolve a pointer to a printable form.
Definition Tracing.h:137
size_t appendToLog(const std::string &line)
Append a line; the returned index pairs with setLogEntry to rewrite the same slot later (TraceRegion'...
Definition Tracing.h:164
std::string writeToFile(const std::string &Version="")
Write the accumulated reproducer log to a file.
Definition Tracing.cpp:196
unsigned nextRetIndex()
Allocate the next _retN index for a vector-return placeholder.
Definition Tracing.h:145
const std::vector< std::string > & getLog() const
Definition Tracing.h:179
std::string getLastLogEntry() const
Definition Tracing.h:180
TraceInfo & operator=(TraceInfo &&)=delete
void StopRegion(const std::string &Version="")
End the traced region and write only the region's entries to the file.
Definition Tracing.cpp:247
void pushTimer(llvm::Timer *T)
Definition Tracing.h:105
bool insideTracedRegion() const
True when at least one TraceRegion is currently active.
Definition Tracing.h:103
std::pair< unsigned, bool > outIndexFor(const void *Addr)
Resolve an OUT-container source address to its _outN index.
Definition Tracing.h:152
TraceInfo & operator=(const TraceInfo &)=delete
void setLogEntry(size_t idx, const std::string &line)
Definition Tracing.h:173
TraceInfo(TraceInfo &&)=delete
std::string getOrRegisterHandle(const void *p)
Definition Tracing.h:121
std::string StartRegion(bool WriteOnStdErr=true)
Begin a traced region.
Definition Tracing.cpp:223
llvm::Timer & getTimer(llvm::StringRef Name)
Definition Tracing.h:95
TraceRegion(const TraceRegion &)=delete
TraceRegion(const char *Name, Args &&... args)
Definition Tracing.h:566
static std::optional< uint64_t > lookupOutMask(llvm::StringRef Name)
.td-declared OUT-arg bitmask for Name (matches CppName / func), or std::nullopt for non-public-API tr...
Definition Tracing.cpp:62
TraceRegion & operator=(const TraceRegion &)=delete
static unsigned countParams(const char *sig)
Count parameters from a PRETTY_FUNCTION / FUNCSIG string.
Definition Tracing.h:724
static std::string formatOutMaskMismatchMessage(llvm::StringRef Name, uint64_t expected, uint64_t actual)
Pure-function diagnostic for the OUT-mask mismatch.
Definition Tracing.h:833
TraceRegion(TraceRegion &&) noexcept=default
T record(T val)
Record a non-void return value.
Definition Tracing.h:689
static constexpr uint64_t computeOutMask()
Bit i set iff Args[i] is OutParam (i.e.
Definition Tracing.h:774
OutParam MakeOutParam(const Container &C)
Create an OutParam for any container.
Definition Tracing.h:272
void InitTracing()
Activate tracing.
Definition Tracing.cpp:48
constexpr bool is_handle_vector_v
Definition Tracing.h:510
constexpr bool is_handle_v
Detect opaque handle structs (Cpp::DeclRef, Cpp::TypeRef, etc.)
Definition Tracing.h:496
void StopTracing(const std::string &Version="")
End the traced region and write the reproducer file containing only the calls made between StartTraci...
Definition Tracing.h:234
constexpr bool is_pointer_vector_v
Definition Tracing.h:492
std::string StartTracing(bool WriteOnStdErr=true)
Begin recording a traced region.
Definition Tracing.h:226
TraceInfo * TheTraceInfo
Process-global tracer pointer.
Definition Tracing.cpp:46
Marks a function parameter as an output container (e.g.
Definition Tracing.h:250
std::function< void(TraceInfo &, unsigned OutIdx)> RegisterHandles
Register the container's pointer elements as handles and emit one void* vN = _outN[i] : nullptr; decl...
Definition Tracing.h:255
bool IsPointerContainer
Drives the _outN preamble + alias path.
Definition Tracing.h:259
const void * SourceAddr
Address of the source container object (not its data buffer); multiple calls with the same container ...
Definition Tracing.h:267
bool IsScalarPointer
Scalar pointer OUT (e.g. bool*); rendered as nullptr.
Definition Tracing.h:261
Internal helper to stringify arguments into a C++ call format.
Definition Tracing.h:307
void append(unsigned long long v)
Definition Tracing.h:372
llvm::raw_svector_ostream OS
Definition Tracing.h:309
void append(const Cpp::TemplateArgInfo &tai)
Definition Tracing.h:395
void append(Cpp::TypeRef h)
Definition Tracing.h:315
void append(const char *s)
Definition Tracing.h:360
void append(Cpp::ConstDeclRef h)
Definition Tracing.h:319
static std::string parseEnumName(const char *sig)
Definition Tracing.h:410
void append(Cpp::DeclRef h)
Definition Tracing.h:314
void append(Cpp::ConstTypeRef h)
Definition Tracing.h:320
llvm::SmallString< 128 > Buffer
Definition Tracing.h:308
void appendRaw(std::string_view s)
Definition Tracing.h:344
void append(Cpp::InterpRef h)
Definition Tracing.h:318
void append(const void *p)
Definition Tracing.h:324
void format(llvm::ArrayRef< unsigned > OutIndices, Args &&... args)
Format a comma-separated argument list.
Definition Tracing.h:456
void append(const std::string &s)
Definition Tracing.h:363
void append(Cpp::ObjectRef h)
Definition Tracing.h:317
void append(Cpp::FuncRef h)
Definition Tracing.h:316
void append(unsigned long v)
Definition Tracing.h:370
void append(const std::vector< T > &V)
Definition Tracing.h:378
void append(Cpp::ConstFuncRef h)
Definition Tracing.h:321
Holds all the data that is only needed when tracing is active.
Definition Tracing.h:515
bool Nested
Set when another TraceRegion was already active at construction.
Definition Tracing.h:541
llvm::SmallVector< std::function< void(TraceInfo &, unsigned)>, 2 > OutCallbacks
Definition Tracing.h:530
llvm::SmallVector< unsigned, 2 > OutIndices
_outN indices for this call's OUT pointer-containers, in left-to-right order.
Definition Tracing.h:534
bool HasRetVec
Set when the call returned std::vector<P*>; ~TraceRegion uses this to spell auto _retN = Cpp::Foo(....
Definition Tracing.h:523
llvm::SmallString< 128 > ArgStr
Definition Tracing.h:517
size_t LogIndex
Slot for the ctor-emitted placeholder; the dtor rewrites it.
Definition Tracing.h:543
llvm::SmallVector< const void *, 8 > RetVecPtrs
Snapshot of the returned vector's pointer elements, taken at record() time – the source vector goes o...
Definition Tracing.h:527
llvm::SmallVector< bool, 2 > OutFirstUse
Per-OUT argument flag: true on the first call with this source container (preamble decl needed),...
Definition Tracing.h:538
Helper to allow INTEROP_TRACE() to work with zero or more arguments without relying on non-standard #...
Definition Tracing.h:786
TraceRegion operator()(Args &&... args)
Definition Tracing.h:789
Detect vector-of-handle types.
Definition Tracing.h:504
Matches std::vector<T*> for any pointer element type.
Definition Tracing.h:488