[clang] [docs][coroutines] Revamp "Debugging C++ coroutines" (PR #142651)
Adrian Vogelsgesang via cfe-commits
cfe-commits at lists.llvm.org
Sun Jun 29 10:07:02 PDT 2025
================
@@ -301,177 +735,284 @@ optimized to the equivalent of:
std::cout << a+5 << "\n";
}
-It should now be obvious why the value of `__int_32_0` remains unchanged
-throughout the function. It is important to recognize that `__int_32_0`
-does not directly correspond to `a`, but is instead a variable generated
-to assist the compiler in code generation. The variables in an optimized
-coroutine frame should not be thought of as directly representing the
-variables in the C++ source.
-
-Get the suspended points
-========================
-
-An important requirement for debugging coroutines is to understand suspended
-points, which are where the coroutine is currently suspended and awaiting.
-
-For simple cases like the above, inspecting the value of the `__coro_index`
-variable in the coroutine frame works well.
+It should now be obvious why the value of ``__int_32_0`` remains unchanged
+throughout the function. It is important to recognize that ``__int_32_0`` does
+not directly correspond to ``a``, but is instead a variable generated to assist
+the compiler in code generation. The variables in an optimized coroutine frame
+should not be thought of as directly representing the variables in the C++
+source.
-However, it is not quite so simple in really complex situations. In these
-cases, it is necessary to use the coroutine libraries to insert the
-line-number.
-For example:
-
-.. code-block:: c++
+Resources
+=========
- // For all the promise_type we want:
- class promise_type {
- ...
- + unsigned line_number = 0xffffffff;
- };
+.. _lldb-script:
- #include <source_location>
+LLDB Debugger Script
+--------------------
- // For all the awaiter types we need:
- class awaiter {
- ...
- template <typename Promise>
- void await_suspend(std::coroutine_handle<Promise> handle,
- std::source_location sl = std::source_location::current()) {
- ...
- handle.promise().line_number = sl.line();
- }
- };
+The following script provides the ``coro bt`` and ``coro in-flight`` commands
+discussed above. It can be loaded into LLDB using ``command script import
+lldb_coro_debugging.py``. To load this by default, add this command to your
+``~/.lldbinit`` file.
-In this case, we use `std::source_location` to store the line number of the
-await inside the `promise_type`. Since we can locate the coroutine function
-from the address of the coroutine, we can identify suspended points this way
-as well.
+Note that this script requires LLDB 21.0 or newer.
-The downside here is that this comes at the price of additional runtime cost.
-This is consistent with the C++ philosophy of "Pay for what you use".
-
-Get the asynchronous stack
-==========================
-
-Another important requirement to debug a coroutine is to print the asynchronous
-stack to identify the asynchronous caller of the coroutine. As many
-implementations of coroutine types store `std::coroutine_handle<> continuation`
-in the promise type, identifying the caller should be trivial. The
-`continuation` is typically the awaiting coroutine for the current coroutine.
-That is, the asynchronous parent.
-
-Since the `promise_type` is obtainable from the address of a coroutine and
-contains the corresponding continuation (which itself is a coroutine with a
-`promise_type`), it should be trivial to print the entire asynchronous stack.
-
-This logic should be quite easily captured in a debugger script.
-
-Examples to print asynchronous stack
-------------------------------------
-
-Here is an example to print the asynchronous stack for the normal task implementation.
+.. code-block:: python
-.. code-block:: c++
+ # lldb_coro_debugging.py
+ import lldb
+ from lldb.plugins.parsed_cmd import ParsedCommand
+
+ def _get_first_var_path(v, paths):
+ """
+ Tries multiple variable paths via `GetValueForExpressionPath`
+ and returns the first one that succeeds, or None if none succeed.
+ """
+ for path in paths:
+ var = v.GetValueForExpressionPath(path)
+ if var.error.Success():
+ return var
+ return None
+
+
+ def _print_async_bt(coro_hdl, result, *, curr_idx, start, limit, continuation_paths, prefix=""):
+ """
+ Prints a backtrace for an async coroutine stack starting from `coro_hdl`,
+ using the given `continuation_paths` to get the next coroutine from the promise.
+ """
+ target = coro_hdl.GetTarget()
+ while curr_idx < limit and coro_hdl is not None and coro_hdl.error.Success():
+ # Print the stack frame, if in range
+ if curr_idx >= start:
+ # Figure out the function name
+ destroy_func_var = coro_hdl.GetValueForExpressionPath(".destroy")
+ destroy_addr = target.ResolveLoadAddress(destroy_func_var.GetValueAsAddress())
+ func_name = destroy_addr.function.name
+ # Figure out the line entry to show
+ suspension_addr_var = coro_hdl.GetValueForExpressionPath(".promise._coro_suspension_point_addr")
+ if suspension_addr_var.error.Success():
+ line_entry = target.ResolveLoadAddress(suspension_addr_var.GetValueAsAddress()).line_entry
+ print(f"{prefix} frame #{curr_idx}: {func_name} at {line_entry}", file=result)
+ else:
+ # We don't know the exact line, print the suspension point ID, so we at least show
+ # the id of the current suspension point
+ suspension_point_var = coro_hdl.GetValueForExpressionPath(".coro_frame.__coro_index")
+ if suspension_point_var.error.Success():
+ suspension_point = suspension_point_var.GetValueAsUnsigned()
+ else:
+ suspension_point = "unknown"
+ line_entry = destroy_addr.line_entry
+ print(f"{prefix} frame #{curr_idx}: {func_name} at {line_entry}, suspension point {suspension_point}", file=result)
+ # Move to the next stack frame
+ curr_idx += 1
+ promise_var = coro_hdl.GetChildMemberWithName("promise")
+ coro_hdl = _get_first_var_path(promise_var, continuation_paths)
+ return curr_idx
+
+ def _print_combined_bt(frame, result, *, unfiltered, curr_idx, start, limit, continuation_paths):
+ """
+ Prints a backtrace starting from `frame`, interleaving async coroutine frames
+ with regular frames.
+ """
+ while curr_idx < limit and frame.IsValid():
+ if curr_idx >= start and (unfiltered or not frame.IsHidden()):
+ print(f"frame #{curr_idx}: {frame.name} at {frame.line_entry}", file=result)
+ curr_idx += 1
+ coro_var = _get_first_var_path(frame.GetValueForVariablePath("__promise"), continuation_paths)
+ if coro_var:
+ curr_idx = _print_async_bt(coro_var, result,
+ curr_idx=curr_idx, start=start, limit=limit,
+ continuation_paths=continuation_paths, prefix="[async]")
+ frame = frame.parent
+
+
+ class CoroBacktraceCommand(ParsedCommand):
+ def get_short_help(self):
+ return "Create a backtrace for C++-20 coroutines"
+
+ def get_flags(self):
+ return lldb.eCommandRequiresFrame | lldb.eCommandProcessMustBePaused
+
+ def setup_command_definition(self):
+ ov_parser = self.get_parser()
+ ov_parser.add_option(
+ "e",
+ "continuation-expr",
+ help = (
+ "Semi-colon-separated list of expressions evaluated against the promise object"
+ "to get the next coroutine (e.g. `.continuation;.coro_parent`)"
+ ),
+ value_type = lldb.eArgTypeNone,
+ dest = "continuation_expr_arg",
+ default = ".continuation",
+ )
+ ov_parser.add_option(
+ "c",
+ "count",
+ help = "How many frames to display (0 for all)",
+ value_type = lldb.eArgTypeCount,
+ dest = "count_arg",
+ default = 20,
+ )
+ ov_parser.add_option(
+ "s",
+ "start",
+ help = "Frame in which to start the backtrace",
+ value_type = lldb.eArgTypeIndex,
+ dest = "frame_index_arg",
+ default = 0,
+ )
+ ov_parser.add_option(
+ "u",
+ "unfiltered",
+ help = "Do not filter out frames according to installed frame recognizers",
+ value_type = lldb.eArgTypeBoolean,
+ dest = "unfiltered_arg",
+ default = False,
+ )
+ ov_parser.add_argument_set([
+ ov_parser.make_argument_element(
+ lldb.eArgTypeExpression,
+ repeat="optional"
+ )
+ ])
+
+ def __call__(self, debugger, args_array, exe_ctx, result):
+ ov_parser = self.get_parser()
+ continuation_paths = ov_parser.continuation_expr_arg.split(";")
+ count = ov_parser.count_arg
+ if count == 0:
+ count = 99999
+ frame_index = ov_parser.frame_index_arg
+ unfiltered = ov_parser.unfiltered_arg
+
+ frame = exe_ctx.GetFrame()
+ if not frame.IsValid():
+ result.SetError("invalid frame")
+ return
- // debugging-example.cpp
- #include <coroutine>
- #include <iostream>
- #include <utility>
+ if len(args_array) > 1:
+ result.SetError("At most one expression expected")
+ return
+ elif len(args_array) == 1:
+ expr = args_array.GetItemAtIndex(0).GetStringValue(9999)
+ coro_hdl = frame.EvaluateExpression(expr)
+ if not coro_hdl.error.Success():
+ result.AppendMessage(
+ f'error: expression failed {expr} => {async_root.error}'
+ )
+ result.SetError(f"Expression `{expr}` failed to evaluate")
+ return
+ _print_async_bt(coro_hdl, result,
+ curr_idx = 0, start = frame_index, limit = frame_index + count,
+ continuation_paths = continuation_paths)
+ else:
+ _print_combined_bt(frame, result, unfiltered=unfiltered,
+ curr_idx = 0, start = frame_index, limit = frame_index + count,
+ continuation_paths = continuation_paths)
+
+
+ class Coroin-flightCommand(ParsedCommand):
----------------
vogelsgesang wrote:
accidentally introduced when I did a global `s/inflight/in-flight`. Fixed
https://github.com/llvm/llvm-project/pull/142651
More information about the cfe-commits
mailing list