forked from OSchip/llvm-project
[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:
parent
94e64df576
commit
91389000ab
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
add_lldb_library(lldbPluginCPlusPlusLanguage PLUGIN
|
||||
BlockPointer.cpp
|
||||
Coroutines.cpp
|
||||
CPlusPlusLanguage.cpp
|
||||
CPlusPlusNameParser.cpp
|
||||
CxxStringTypes.cpp
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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
|
|
@ -0,0 +1,4 @@
|
|||
CXX_SOURCES := main.cpp
|
||||
CFLAGS_EXTRAS := -std=c++20
|
||||
|
||||
include Makefile.rules
|
|
@ -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)
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue