[LLDB] Add data formatter for std::coroutine_handle

This patch adds a formatter for `std::coroutine_handle`, both for libc++
and libstdc++. For the type-erased `coroutine_handle<>`, it shows the
`resume` and `destroy` function pointers. For a non-type-erased
`coroutine_handle<promise_type>` it also shows the `promise` value.

With this change, executing the `v t` command on the example from
https://clang.llvm.org/docs/DebuggingCoroutines.html now outputs

```
(task) t = {
  handle = coro frame = 0x55555555b2a0 {
    resume = 0x0000555555555a10 (a.out`coro_task(int, int) at llvm-example.cpp:36)
    destroy = 0x0000555555556090 (a.out`coro_task(int, int) at llvm-example.cpp:36)
  }
}
```

instead of just

```
(task) t = {
  handle = {
    __handle_ = 0x55555555b2a0
  }
}
```

Note, how the symbols for the `resume` and `destroy` function pointer
reveal which coroutine is stored inside the `std::coroutine_handle`.
A follow-up commit will use this fact to infer the coroutine's promise
type and the representation of its internal coroutine state based on
the `resume` and `destroy` pointers.

The same formatter is used for both libc++ and libstdc++. It would
also work for MSVC's standard library, however it is not registered
for MSVC, given that lldb does not provide pretty printers for other
MSVC types, either.

The formatter is in a newly added  `Coroutines.{h,cpp}` file because there
does not seem to be an already existing place where we could share
formatters across libc++ and libstdc++. Also, I expect this code to grow
as we improve debugging experience for coroutines further.

**Testing**

* Added API test

Differential Revision: https://reviews.llvm.org/D132415
This commit is contained in:
Adrian Vogelsgesang 2022-08-21 10:50:52 -07:00
parent 94e64df576
commit 91389000ab
10 changed files with 367 additions and 2 deletions

View File

@ -4180,6 +4180,8 @@ lldb/source/Plugins/Language/ClangCommon/ClangHighlighter.cpp
lldb/source/Plugins/Language/ClangCommon/ClangHighlighter.h
lldb/source/Plugins/Language/CPlusPlus/BlockPointer.cpp
lldb/source/Plugins/Language/CPlusPlus/BlockPointer.h
lldb/source/Plugins/Language/CPlusPlus/Coroutines.cpp
lldb/source/Plugins/Language/CPlusPlus/Coroutines.h
lldb/source/Plugins/Language/CPlusPlus/CPlusPlusLanguage.h
lldb/source/Plugins/Language/CPlusPlus/CPlusPlusNameParser.h
lldb/source/Plugins/Language/CPlusPlus/CxxStringTypes.h

View File

@ -292,8 +292,12 @@ class ValueCheck:
test_base.assertEqual(self.expect_type, val.GetDisplayTypeName(),
this_error_msg)
if self.expect_summary:
test_base.assertEqual(self.expect_summary, val.GetSummary(),
this_error_msg)
if isinstance(self.expect_summary, re.Pattern):
test_base.assertRegex(val.GetSummary(), self.expect_summary,
this_error_msg)
else:
test_base.assertEqual(self.expect_summary, val.GetSummary(),
this_error_msg)
if self.children is not None:
self.check_value_children(test_base, val, error_msg)

View File

@ -988,6 +988,21 @@ def continue_to_breakpoint(process, bkpt):
return get_threads_stopped_at_breakpoint(process, bkpt)
def continue_to_source_breakpoint(test, process, bkpt_pattern, source_spec):
"""
Sets a breakpoint set by source regex bkpt_pattern, continues the process, and deletes the breakpoint again.
Otherwise the same as `continue_to_breakpoint`
"""
breakpoint = process.target.BreakpointCreateBySourceRegex(
bkpt_pattern, source_spec, None)
test.assertTrue(breakpoint.GetNumLocations() > 0,
'No locations found for source breakpoint: "%s", file: "%s", dir: "%s"'
%(bkpt_pattern, source_spec.GetFilename(), source_spec.GetDirectory()))
stopped_threads = continue_to_breakpoint(process, breakpoint)
process.target.BreakpointDelete(breakpoint.GetID())
return stopped_threads
def get_caller_symbol(thread):
"""
Returns the symbol name for the call site of the leaf function.

View File

@ -1,5 +1,6 @@
add_lldb_library(lldbPluginCPlusPlusLanguage PLUGIN
BlockPointer.cpp
Coroutines.cpp
CPlusPlusLanguage.cpp
CPlusPlusNameParser.cpp
CxxStringTypes.cpp

View File

@ -35,6 +35,7 @@
#include "BlockPointer.h"
#include "CPlusPlusNameParser.h"
#include "Coroutines.h"
#include "CxxStringTypes.h"
#include "Generic.h"
#include "LibCxx.h"
@ -796,6 +797,14 @@ static void LoadLibCxxFormatters(lldb::TypeCategoryImplSP cpp_category_sp) {
ConstString("^std::__[[:alnum:]]+::function<.+>$"),
stl_summary_flags, true);
ConstString libcxx_std_coroutine_handle_regex(
"^std::__[[:alnum:]]+::coroutine_handle<.+>(( )?&)?$");
AddCXXSynthetic(
cpp_category_sp,
lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEndCreator,
"coroutine_handle synthetic children", libcxx_std_coroutine_handle_regex,
stl_deref_flags, true);
stl_summary_flags.SetDontShowChildren(false);
stl_summary_flags.SetSkipPointers(false);
AddCXXSummary(cpp_category_sp,
@ -898,6 +907,11 @@ static void LoadLibCxxFormatters(lldb::TypeCategoryImplSP cpp_category_sp) {
"libc++ std::unique_ptr summary provider",
libcxx_std_unique_ptr_regex, stl_summary_flags, true);
AddCXXSummary(cpp_category_sp,
lldb_private::formatters::StdlibCoroutineHandleSummaryProvider,
"libc++ std::coroutine_handle summary provider",
libcxx_std_coroutine_handle_regex, stl_summary_flags, true);
AddCXXSynthetic(
cpp_category_sp,
lldb_private::formatters::LibCxxVectorIteratorSyntheticFrontEndCreator,
@ -1122,6 +1136,14 @@ static void LoadLibStdcppFormatters(lldb::TypeCategoryImplSP cpp_category_sp) {
"std::tuple synthetic children", ConstString("^std::tuple<.+>(( )?&)?$"),
stl_synth_flags, true);
ConstString libstdcpp_std_coroutine_handle_regex(
"^std::coroutine_handle<.+>(( )?&)?$");
AddCXXSynthetic(
cpp_category_sp,
lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEndCreator,
"std::coroutine_handle synthetic children",
libstdcpp_std_coroutine_handle_regex, stl_deref_flags, true);
AddCXXSynthetic(
cpp_category_sp,
lldb_private::formatters::LibStdcppBitsetSyntheticFrontEndCreator,
@ -1149,6 +1171,10 @@ static void LoadLibStdcppFormatters(lldb::TypeCategoryImplSP cpp_category_sp) {
"libstdc++ std::weak_ptr summary provider",
ConstString("^std::weak_ptr<.+>(( )?&)?$"), stl_summary_flags,
true);
AddCXXSummary(cpp_category_sp,
lldb_private::formatters::StdlibCoroutineHandleSummaryProvider,
"libstdc++ std::coroutine_handle summary provider",
libstdcpp_std_coroutine_handle_regex, stl_summary_flags, true);
AddCXXSummary(
cpp_category_sp, lldb_private::formatters::GenericOptionalSummaryProvider,
"libstd++ std::optional summary provider",

View File

@ -0,0 +1,137 @@
//===-- Coroutines.cpp ----------------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "Coroutines.h"
#include "Plugins/TypeSystem/Clang/TypeSystemClang.h"
using namespace lldb;
using namespace lldb_private;
using namespace lldb_private::formatters;
static ValueObjectSP GetCoroFramePtrFromHandle(ValueObject &valobj) {
ValueObjectSP valobj_sp(valobj.GetNonSyntheticValue());
if (!valobj_sp)
return nullptr;
// We expect a single pointer in the `coroutine_handle` class.
// We don't care about its name.
if (valobj_sp->GetNumChildren() != 1)
return nullptr;
ValueObjectSP ptr_sp(valobj_sp->GetChildAtIndex(0, true));
if (!ptr_sp)
return nullptr;
if (!ptr_sp->GetCompilerType().IsPointerType())
return nullptr;
return ptr_sp;
}
static CompilerType GetCoroutineFrameType(TypeSystemClang &ast_ctx,
CompilerType promise_type) {
CompilerType void_type = ast_ctx.GetBasicType(lldb::eBasicTypeVoid);
CompilerType coro_func_type = ast_ctx.CreateFunctionType(
/*result_type=*/void_type, /*args=*/&void_type, /*num_args=*/1,
/*is_variadic=*/false, /*qualifiers=*/0);
CompilerType coro_abi_type;
if (promise_type.IsVoidType()) {
coro_abi_type = ast_ctx.CreateStructForIdentifier(
ConstString(), {{"resume", coro_func_type.GetPointerType()},
{"destroy", coro_func_type.GetPointerType()}});
} else {
coro_abi_type = ast_ctx.CreateStructForIdentifier(
ConstString(), {{"resume", coro_func_type.GetPointerType()},
{"destroy", coro_func_type.GetPointerType()},
{"promise", promise_type}});
}
return coro_abi_type;
}
bool lldb_private::formatters::StdlibCoroutineHandleSummaryProvider(
ValueObject &valobj, Stream &stream, const TypeSummaryOptions &options) {
ValueObjectSP ptr_sp(GetCoroFramePtrFromHandle(valobj));
if (!ptr_sp)
return false;
stream.Printf("coro frame = 0x%" PRIx64, ptr_sp->GetValueAsUnsigned(0));
return true;
}
lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd::
StdlibCoroutineHandleSyntheticFrontEnd(lldb::ValueObjectSP valobj_sp)
: SyntheticChildrenFrontEnd(*valobj_sp) {
if (valobj_sp)
Update();
}
lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd::
~StdlibCoroutineHandleSyntheticFrontEnd() = default;
size_t lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd::
CalculateNumChildren() {
if (!m_frame_ptr_sp)
return 0;
return m_frame_ptr_sp->GetNumChildren();
}
lldb::ValueObjectSP lldb_private::formatters::
StdlibCoroutineHandleSyntheticFrontEnd::GetChildAtIndex(size_t idx) {
if (!m_frame_ptr_sp)
return lldb::ValueObjectSP();
return m_frame_ptr_sp->GetChildAtIndex(idx, true);
}
bool lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd::
Update() {
m_frame_ptr_sp.reset();
ValueObjectSP valobj_sp = m_backend.GetSP();
if (!valobj_sp)
return false;
ValueObjectSP ptr_sp(GetCoroFramePtrFromHandle(m_backend));
if (!ptr_sp)
return false;
TypeSystemClang *ast_ctx = llvm::dyn_cast_or_null<TypeSystemClang>(
valobj_sp->GetCompilerType().GetTypeSystem());
if (!ast_ctx)
return false;
CompilerType promise_type(
valobj_sp->GetCompilerType().GetTypeTemplateArgument(0));
if (!promise_type)
return false;
CompilerType coro_frame_type = GetCoroutineFrameType(*ast_ctx, promise_type);
m_frame_ptr_sp = ptr_sp->Cast(coro_frame_type.GetPointerType());
return false;
}
bool lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEnd::
MightHaveChildren() {
return true;
}
size_t StdlibCoroutineHandleSyntheticFrontEnd::GetIndexOfChildWithName(
ConstString name) {
if (!m_frame_ptr_sp)
return UINT32_MAX;
return m_frame_ptr_sp->GetIndexOfChildWithName(name);
}
SyntheticChildrenFrontEnd *
lldb_private::formatters::StdlibCoroutineHandleSyntheticFrontEndCreator(
CXXSyntheticChildren *, lldb::ValueObjectSP valobj_sp) {
return (valobj_sp ? new StdlibCoroutineHandleSyntheticFrontEnd(valobj_sp)
: nullptr);
}

View File

@ -0,0 +1,57 @@
//===-- Coroutines.h --------------------------------------------*- C++ -*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#ifndef LLDB_SOURCE_PLUGINS_LANGUAGE_CPLUSPLUS_COROUTINES_H
#define LLDB_SOURCE_PLUGINS_LANGUAGE_CPLUSPLUS_COROUTINES_H
#include "lldb/Core/ValueObject.h"
#include "lldb/DataFormatters/TypeSummary.h"
#include "lldb/DataFormatters/TypeSynthetic.h"
#include "lldb/Utility/Stream.h"
namespace lldb_private {
namespace formatters {
/// Summary provider for `std::coroutine_handle<T>` from libc++, libstdc++ and
/// MSVC STL.
bool StdlibCoroutineHandleSummaryProvider(ValueObject &valobj, Stream &stream,
const TypeSummaryOptions &options);
/// Synthetic children frontend for `std::coroutine_handle<promise_type>` from
/// libc++, libstdc++ and MSVC STL. Shows the compiler-generated `resume` and
/// `destroy` function pointers as well as the `promise`, if the promise type
/// is `promise_type != void`.
class StdlibCoroutineHandleSyntheticFrontEnd
: public SyntheticChildrenFrontEnd {
public:
StdlibCoroutineHandleSyntheticFrontEnd(lldb::ValueObjectSP valobj_sp);
~StdlibCoroutineHandleSyntheticFrontEnd() override;
size_t CalculateNumChildren() override;
lldb::ValueObjectSP GetChildAtIndex(size_t idx) override;
bool Update() override;
bool MightHaveChildren() override;
size_t GetIndexOfChildWithName(ConstString name) override;
private:
lldb::ValueObjectSP m_frame_ptr_sp;
};
SyntheticChildrenFrontEnd *
StdlibCoroutineHandleSyntheticFrontEndCreator(CXXSyntheticChildren *,
lldb::ValueObjectSP);
} // namespace formatters
} // namespace lldb_private
#endif // LLDB_SOURCE_PLUGINS_LANGUAGE_CPLUSPLUS_COROUTINES_H

View File

@ -0,0 +1,4 @@
CXX_SOURCES := main.cpp
CFLAGS_EXTRAS := -std=c++20
include Makefile.rules

View File

@ -0,0 +1,79 @@
"""
Test lldb data formatter subsystem.
"""
import lldb
from lldbsuite.test.decorators import *
from lldbsuite.test.lldbtest import *
from lldbsuite.test import lldbutil
USE_LIBSTDCPP = "USE_LIBSTDCPP"
USE_LIBCPP = "USE_LIBCPP"
class TestCoroutineHandle(TestBase):
def do_test(self, stdlib_type):
"""Test std::coroutine_handle is displayed correctly."""
self.build(dictionary={stdlib_type: "1"})
test_generator_func_ptr_re = re.compile(
r"^\(a.out`my_generator_func\(\) at main.cpp:[0-9]*\)$")
# Run until the initial suspension point
lldbutil.run_to_source_breakpoint(self, '// Break at initial_suspend',
lldb.SBFileSpec("main.cpp", False))
# Check that we show the correct function pointers and the `promise`.
self.expect_expr("gen.hdl",
result_summary=re.compile("^coro frame = 0x[0-9a-f]*$"),
result_children=[
ValueCheck(name="resume", summary = test_generator_func_ptr_re),
ValueCheck(name="destroy", summary = test_generator_func_ptr_re),
ValueCheck(name="promise", children=[
ValueCheck(name="current_value", value = "-1"),
])
])
# For type-erased `coroutine_handle<>` we are missing the `promise`
# but still show `resume` and `destroy`.
self.expect_expr("type_erased_hdl",
result_summary=re.compile("^coro frame = 0x[0-9a-f]*$"),
result_children=[
ValueCheck(name="resume", summary = test_generator_func_ptr_re),
ValueCheck(name="destroy", summary = test_generator_func_ptr_re),
])
# Run until after the `co_yield`
process = self.process()
lldbutil.continue_to_source_breakpoint(self, process,
'// Break after co_yield', lldb.SBFileSpec("main.cpp", False))
# We correctly show the updated value inside `prommise.current_value`.
self.expect_expr("gen.hdl",
result_children=[
ValueCheck(name="resume", summary = test_generator_func_ptr_re),
ValueCheck(name="destroy", summary = test_generator_func_ptr_re),
ValueCheck(name="promise", children=[
ValueCheck(name="current_value", value = "42"),
])
])
# Run until the `final_suspend`
lldbutil.continue_to_source_breakpoint(self, process,
'// Break at final_suspend', lldb.SBFileSpec("main.cpp", False))
# At the final suspension point, `resume` is set to a nullptr.
# Check that we still show the remaining data correctly.
self.expect_expr("gen.hdl",
result_children=[
ValueCheck(name="resume", value = "0x0000000000000000"),
ValueCheck(name="destroy", summary = test_generator_func_ptr_re),
ValueCheck(name="promise", children=[
ValueCheck(name="current_value", value = "42"),
])
])
@add_test_categories(["libstdcxx"])
def test_libstdcpp(self):
self.do_test(USE_LIBSTDCPP)
@add_test_categories(["libc++"])
def test_libcpp(self):
self.do_test(USE_LIBCPP)

View File

@ -0,0 +1,40 @@
#include <coroutine>
// `int_generator` is a stripped down, minimal coroutine generator
// type.
struct int_generator {
struct promise_type {
int current_value = -1;
auto get_return_object() {
return std::coroutine_handle<promise_type>::from_promise(*this);
}
auto initial_suspend() { return std::suspend_always(); }
auto final_suspend() noexcept { return std::suspend_always(); }
auto return_void() { return std::suspend_always(); }
void unhandled_exception() { __builtin_unreachable(); }
auto yield_value(int v) {
current_value = v;
return std::suspend_always();
}
};
std::coroutine_handle<promise_type> hdl;
int_generator(std::coroutine_handle<promise_type> h) : hdl(h) {}
~int_generator() { hdl.destroy(); }
};
int_generator my_generator_func() { co_yield 42; }
// This is an empty function which we call just so the debugger has
// a place to reliably set a breakpoint on.
void empty_function_so_we_can_set_a_breakpoint() {}
int main() {
int_generator gen = my_generator_func();
std::coroutine_handle<> type_erased_hdl = gen.hdl;
gen.hdl.resume(); // Break at initial_suspend
gen.hdl.resume(); // Break after co_yield
empty_function_so_we_can_set_a_breakpoint(); // Break at final_suspend
}