Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[lldb] Swift OS plugin #9839

Open
wants to merge 3 commits into
base: stable/20240723
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lldb/include/lldb/Target/Target.h
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,8 @@ class TargetProperties : public Properties {

AutoBool GetSwiftPCMValidation() const;

bool GetSwiftUseTasksPlugin() const;

Args GetSwiftPluginServerForPath() const;

bool GetSwiftAutoImportFrameworks() const;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@ const char *SwiftLanguageRuntime::GetStandardLibraryBaseName() {
return "swiftCore";
}

const char *SwiftLanguageRuntime::GetConcurrencyLibraryBaseName() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Can this return a StringRef or StringLiteral ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could, for sure, but it would break with the pattern of the other functions above. We can do a cleanup and change all of them in a separate PR

return "swift_Concurrency";
}

static ConstString GetStandardLibraryName(Process &process) {
// This result needs to be stored in the constructor.
PlatformSP platform_sp(process.GetTarget().GetPlatform());
Expand All @@ -100,6 +104,14 @@ static ConstString GetStandardLibraryName(Process &process) {
return {};
}

static ConstString GetConcurrencyLibraryName(Process &process) {
PlatformSP platform_sp = process.GetTarget().GetPlatform();
if (platform_sp)
return platform_sp->GetFullNameForDylib(
ConstString(SwiftLanguageRuntime::GetConcurrencyLibraryBaseName()));
return {};
}

ConstString SwiftLanguageRuntime::GetStandardLibraryName() {
return ::GetStandardLibraryName(*m_process);
}
Expand All @@ -109,6 +121,12 @@ static bool IsModuleSwiftRuntime(lldb_private::Process &process,
return module.GetFileSpec().GetFilename() == GetStandardLibraryName(process);
}

static bool IsModuleSwiftConcurrency(lldb_private::Process &process,
lldb_private::Module &module) {
return module.GetFileSpec().GetFilename() ==
GetConcurrencyLibraryName(process);
}

AppleObjCRuntimeV2 *
SwiftLanguageRuntime::GetObjCRuntime(lldb_private::Process &process) {
if (auto objc_runtime = ObjCLanguageRuntime::Get(process)) {
Expand All @@ -131,6 +149,11 @@ static bool IsStaticSwiftRuntime(Module &image) {
return image.FindFirstSymbolWithNameAndType(swift_reflection_version_sym);
}

static bool IsStaticSwiftConcurrency(Module &image) {
static const ConstString task_switch_symbol("_swift_task_switch");

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be:

Suggested change
static const ConstString task_switch_symbol("_swift_task_switch");
static constexpr llvm::StringLiteral task_switch_symbol = "_swift_task_switch";

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rationale here was also just to keep uniformity with the other identical helper functions in this file. But also the call image.FindFirstSymbolWithNameAndType requires a ConstString, so if we have a StringLiteral we would be instantiating a ConstString on every call to this method, as opposed to just once.

return image.FindFirstSymbolWithNameAndType(task_switch_symbol);
}

/// \return the Swift or Objective-C runtime found in the loaded images.
static ModuleSP findRuntime(Process &process, RuntimeKind runtime_kind) {
AppleObjCRuntimeV2 *objc_runtime = nullptr;
Expand Down Expand Up @@ -168,6 +191,52 @@ static ModuleSP findRuntime(Process &process, RuntimeKind runtime_kind) {
return runtime_image;
}

ModuleSP SwiftLanguageRuntime::FindConcurrencyModule(Process &process) {
ModuleSP concurrency_module;
process.GetTarget().GetImages().ForEach([&](const ModuleSP &candidate) {
if (candidate && IsModuleSwiftConcurrency(process, *candidate)) {
concurrency_module = candidate;
return false;
}
return true;
});
if (concurrency_module)
return concurrency_module;

// Do a more expensive search for a statically linked Swift runtime.
process.GetTarget().GetImages().ForEach([&](const ModuleSP &candidate) {
if (candidate && IsStaticSwiftConcurrency(*candidate)) {
concurrency_module = candidate;
return false;
}
return true;
});
return concurrency_module;
}

std::optional<uint32_t>
SwiftLanguageRuntime::FindConcurrencyDebugVersion(Process &process) {
ModuleSP concurrency_module = FindConcurrencyModule(process);
if (!concurrency_module)
return {};

const Symbol *version_symbol =
concurrency_module->FindFirstSymbolWithNameAndType(
ConstString("_swift_concurrency_debug_internal_layout_version"));
if (!version_symbol)
return 0;

addr_t symbol_addr = version_symbol->GetLoadAddress(&process.GetTarget());
if (symbol_addr == LLDB_INVALID_ADDRESS)
return {};
Status error;
uint64_t version = process.ReadUnsignedIntegerFromMemory(
symbol_addr, /*width*/ 4, /*fail_value=*/0, error);
if (error.Fail())
return {};
return version;
}

static std::optional<lldb::addr_t>
FindSymbolForSwiftObject(Process &process, RuntimeKind runtime_kind,
StringRef object, const SymbolType sym_type) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,16 @@ class SwiftLanguageRuntime : public LanguageRuntime {
static SwiftLanguageRuntime *Get(lldb::ProcessSP process_sp) {
return SwiftLanguageRuntime::Get(process_sp.get());
}

/// Returns the Module containing the Swift Concurrency runtime, if it exists.
static lldb::ModuleSP FindConcurrencyModule(Process &process);

/// Returns the version of the swift concurrency runtime debug layout.
/// If no Concurrency module is found, or if errors occur, nullopt is
/// returned.
/// Returns 0 for versions of the module prior to the introduction
/// of versioning.
static std::optional<uint32_t> FindConcurrencyDebugVersion(Process &process);
/// \}

/// PluginInterface protocol.
Expand Down Expand Up @@ -481,6 +491,8 @@ class SwiftLanguageRuntime : public LanguageRuntime {
static const char *GetErrorBackstopName();
ConstString GetStandardLibraryName();
static const char *GetStandardLibraryBaseName();
static const char *GetConcurrencyLibraryBaseName();

static bool IsSwiftClassName(const char *name);
/// Determines wether \c variable is the "self" object.
static bool IsSelf(Variable &variable);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,7 @@ class ThreadPlanRunToAddressOnAsyncCtx : public ThreadPlan {
auto &target = thread.GetProcess()->GetTarget();
m_funclet_bp = target.CreateBreakpoint(destination_addr, true, false);
m_funclet_bp->SetBreakpointKind("async-run-to-funclet");
m_funclet_bp->SetThreadID(thread.GetID());
Copy link
Author

@felipepiovezan felipepiovezan Jan 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to refresh reviewer's memories: this is the thread plan to "run through a task switch and stop when we schedule this same async function again. This plan now sets thread specific breakpoints, since they are now task breakpoints!

If we somehow push this type of plan without having the OS plugin, we are at a risk of breaking in the wrong task. But if the plugin is not there and there are tasks, we are going to have much bigger problems anyway. For example, this code has been associated with a lot of LLDB crashes because it triggers stack plan migration (this is a downstream-only thing that becomes dead code with this patch, and I'll delete it in a follow up PR)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should be this an inline comment in the source file ?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would argue that in this case we don't need any comments, because this is just the "natural" thing to do. If anything, it is the old implementation that deserved a comment, as it was essentially making all threads stop.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This plan now sets thread specific breakpoints, since they are now task breakpoints!

I'm not sure I understand what you're saying with this.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noticed that the writing was indeed confusing, my bad.
The important thing to note here is that this plan is no longer special, it should work like any other plan: when it sets a breakpoint, it sets a thread specific breakpoint (otherwise it would cause all threads to stop).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A follow up patch will remove this plan in favor of the RunToBreakpointPlan

}

bool ValidatePlan(Stream *error) override {
Expand Down
1 change: 1 addition & 0 deletions lldb/source/Plugins/OperatingSystem/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
if (LLDB_ENABLE_PYTHON)
add_subdirectory(Python)
add_subdirectory(SwiftTasks)
endif()
12 changes: 12 additions & 0 deletions lldb/source/Plugins/OperatingSystem/SwiftTasks/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
add_lldb_library(lldbPluginOperatingSystemSwiftTasks PLUGIN
OperatingSystemSwiftTasks.cpp

LINK_LIBS
lldbCore
lldbInterpreter
lldbSymbol
lldbTarget
lldbValueObject
lldbPluginProcessUtility
lldbPluginSwiftLanguageRuntime
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
//===-- OperatingSystemSwiftTasks.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
//
//===----------------------------------------------------------------------===//

#if LLDB_ENABLE_SWIFT

#include "OperatingSystemSwiftTasks.h"
#include "Plugins/LanguageRuntime/Swift/SwiftLanguageRuntime.h"
#include "Plugins/Process/Utility/ThreadMemory.h"
#include "lldb/Core/Debugger.h"
#include "lldb/Core/Module.h"
#include "lldb/Core/PluginManager.h"
#include "lldb/Target/Process.h"
#include "lldb/Target/Thread.h"
#include "lldb/Target/ThreadList.h"
#include "lldb/Utility/LLDBLog.h"

#include <memory>

using namespace lldb;
using namespace lldb_private;

LLDB_PLUGIN_DEFINE(OperatingSystemSwiftTasks)

void OperatingSystemSwiftTasks::Initialize() {
PluginManager::RegisterPlugin(GetPluginNameStatic(),
GetPluginDescriptionStatic(), CreateInstance,
nullptr);
}

void OperatingSystemSwiftTasks::Terminate() {
PluginManager::UnregisterPlugin(CreateInstance);
}

OperatingSystem *OperatingSystemSwiftTasks::CreateInstance(Process *process,
bool force) {
if (!process || !process->GetTarget().GetSwiftUseTasksPlugin())
return nullptr;

Log *log = GetLog(LLDBLog::OS);
std::optional<uint32_t> concurrency_version =
SwiftLanguageRuntime::FindConcurrencyDebugVersion(*process);
if (!concurrency_version) {
LLDB_LOG(log,
"OperatingSystemSwiftTasks: did not find concurrency module.");
return nullptr;
}

if (*concurrency_version <= 1)
return new OperatingSystemSwiftTasks(*process);
LLDB_LOGF(log,
"OperatingSystemSwiftTasks: got a concurrency version symbol of %u",
*concurrency_version);
return nullptr;
}

llvm::StringRef OperatingSystemSwiftTasks::GetPluginDescriptionStatic() {
return "Operating system plug-in converting Swift Tasks into Threads.";
}

OperatingSystemSwiftTasks::~OperatingSystemSwiftTasks() = default;

OperatingSystemSwiftTasks::OperatingSystemSwiftTasks(
lldb_private::Process &process)
: OperatingSystem(&process) {
size_t ptr_size = process.GetAddressByteSize();
// Offset of a Task ID inside a Task data structure, guaranteed by the ABI.
// See Job in swift/RemoteInspection/RuntimeInternals.h.
m_task_id_offset = 4 * ptr_size + 4;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this the job id? if so should the name reflect that? also if so, do we know if we can ignore the task's upper 32 bit identifier?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on what I saw in the runtime code, I think it's safe to do so. It is what is being done in other tools anyway.

I'll rename this to "job id"!

}

bool OperatingSystemSwiftTasks::UpdateThreadList(ThreadList &old_thread_list,
ThreadList &core_thread_list,
ThreadList &new_thread_list) {
Log *log = GetLog(LLDBLog::OS);
LLDB_LOG(log, "OperatingSystemSwiftTasks: Updating thread list");

for (const ThreadSP &real_thread : core_thread_list.Threads()) {
std::optional<uint64_t> task_id = FindTaskId(*real_thread);

// If this is not a thread running a Task, add it to the list as is.
if (!task_id) {
new_thread_list.AddThread(real_thread);
LLDB_LOGF(log,
"OperatingSystemSwiftTasks: thread %" PRIx64
" is not executing a Task",
real_thread->GetID());
continue;
}

// Mask higher bits to avoid conflicts with core thread IDs.
uint64_t masked_task_id = 0x0000000f00000000 | *task_id;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice hack :p

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest I don't think we need to / should do this, because conflicts are handled without issues... it is just a way to visually know we have a fake thread.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

how are the conflicts handled?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a couple of lines down, on line 100


ThreadSP swift_thread = [&]() -> ThreadSP {
// If we already had a thread for this Task in the last stop, re-use it.
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id);
IsOperatingSystemPluginThread(old_thread))
Comment on lines +100 to +101

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First time I'm seeing this syntax :p Don't you want to run the second part of the if condition only if the first one if valid ?

Suggested change
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id);
IsOperatingSystemPluginThread(old_thread))
if ((ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id)) && IsOperatingSystemPluginThread(old_thread))

Copy link
Author

@felipepiovezan felipepiovezan Feb 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IsOperatingSystemPluginThread is designed to take a pointer argument, so it knows how to handle nullptrs.

return old_thread;

std::string name = llvm::formatv("Swift Task {0:x}", *task_id);
llvm::StringRef queue_name = "";
return std::make_shared<ThreadMemory>(*m_process, masked_task_id, name,
queue_name,
/*register_data_addr*/ 0);
}();
Comment on lines +98 to +109

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The lambda through me off. Is there something I'm missing that it can't be an if/else?

Suggested change
ThreadSP swift_thread = [&]() -> ThreadSP {
// If we already had a thread for this Task in the last stop, re-use it.
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id);
IsOperatingSystemPluginThread(old_thread))
return old_thread;
std::string name = llvm::formatv("Swift Task {0:x}", *task_id);
llvm::StringRef queue_name = "";
return std::make_shared<ThreadMemory>(*m_process, masked_task_id, name,
queue_name,
/*register_data_addr*/ 0);
}();
ThreadSP swift_thread;
// If we already had a thread for this Task in the last stop, re-use it.
if (ThreadSP old_thread = old_thread_list.FindThreadByID(masked_task_id);
IsOperatingSystemPluginThread(old_thread)) {
swift_thread = old_thread;
} else {
std::string name = llvm::formatv("Swift Task {0:x}", *task_id);
llvm::StringRef queue_name = "";
swift_thread = std::make_shared<ThreadMemory>(*m_process, masked_task_id, name,
queue_name,
/*register_data_addr*/ 0);
}


swift_thread->SetBackingThread(real_thread);
new_thread_list.AddThread(swift_thread);
LLDB_LOGF(log,
"OperatingSystemSwiftTasks: mapping thread IDs: %" PRIx64
" -> %" PRIx64,
real_thread->GetID(), swift_thread->GetID());
}
return true;
}

void OperatingSystemSwiftTasks::ThreadWasSelected(Thread *thread) {}

RegisterContextSP OperatingSystemSwiftTasks::CreateRegisterContextForThread(
Thread *thread, addr_t reg_data_addr) {
if (!thread || !IsOperatingSystemPluginThread(thread->shared_from_this()))
return nullptr;
return thread->GetRegisterContext();
}

StopInfoSP OperatingSystemSwiftTasks::CreateThreadStopReason(
lldb_private::Thread *thread) {
return thread->GetStopInfo();
}

std::optional<uint64_t> OperatingSystemSwiftTasks::FindTaskId(Thread &thread) {
llvm::Expected<addr_t> task_addr = GetTaskAddrFromThreadLocalStorage(thread);
if (!task_addr) {
LLDB_LOG_ERROR(GetLog(LLDBLog::OS), task_addr.takeError(),
"OperatingSystemSwiftTasks: failed to find task address in "
"thread local storage: {0}");
return {};
}

Status error;
// The Task ID is at offset m_task_id_offset from the Task pointer.
constexpr uint32_t num_bytes_task_id = 4;
auto task_id = m_process->ReadUnsignedIntegerFromMemory(
*task_addr + m_task_id_offset, num_bytes_task_id, LLDB_INVALID_ADDRESS,
error);
if (error.Fail())
return {};
return task_id;
}

#endif // #if LLDB_ENABLE_SWIFT
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//===-- OperatingSystemSwiftTasks.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 liblldb_OperatingSystemSwiftTasks_h_
#define liblldb_OperatingSystemSwiftTasks_h_

#if LLDB_ENABLE_SWIFT

#include "lldb/Target/OperatingSystem.h"

namespace lldb_private {
class OperatingSystemSwiftTasks : public OperatingSystem {
public:
OperatingSystemSwiftTasks(Process &process);
~OperatingSystemSwiftTasks() override;

static OperatingSystem *CreateInstance(Process *process, bool force);
static void Initialize();
static void Terminate();
static llvm::StringRef GetPluginNameStatic() { return "swift"; }
static llvm::StringRef GetPluginDescriptionStatic();

/// PluginInterface Methods

llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }

/// OperatingSystem Methods

bool UpdateThreadList(ThreadList &old_thread_list,
ThreadList &real_thread_list,
ThreadList &new_thread_list) override;

void ThreadWasSelected(Thread *thread) override;

lldb::RegisterContextSP
CreateRegisterContextForThread(Thread *thread,
lldb::addr_t reg_data_addr) override;

lldb::StopInfoSP CreateThreadStopReason(Thread *thread) override;

bool DoesPluginReportAllThreads() override { return false; }

private:
/// Find the Task ID of the task being executed by `thread`, if any.
std::optional<uint64_t> FindTaskId(Thread &thread);

/// The offset of the Task ID inside a Task data structure.
size_t m_task_id_offset;
};
} // namespace lldb_private

#endif // LLDB_ENABLE_SWIFT

#endif // liblldb_OperatingSystemSwiftTasks_h_
11 changes: 11 additions & 0 deletions lldb/source/Target/Target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4496,6 +4496,17 @@ AutoBool TargetProperties::GetSwiftPCMValidation() const {
return AutoBool::Auto;
}

bool TargetProperties::GetSwiftUseTasksPlugin() const {
const Property *exp_property =
m_collection_sp->GetPropertyAtIndex(ePropertyExperimental);
OptionValueProperties *exp_values =
exp_property->GetValue()->GetAsProperties();
if (exp_values)
return exp_values->GetPropertyAtIndexAs<bool>(ePropertySwiftUseTasksPlugin)
.value_or(true);
return true;
}

Args TargetProperties::GetSwiftPluginServerForPath() const {
const uint32_t idx = ePropertySwiftPluginServerForPath;

Expand Down
Loading