Skip to content

[lldb] Adding pipe support to lldb_private::MainLoopWindows. #145621

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

Merged
merged 8 commits into from
Jul 1, 2025
Merged
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
17 changes: 15 additions & 2 deletions lldb/include/lldb/Host/windows/MainLoopWindows.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,30 @@ class MainLoopWindows : public MainLoopBase {

Status Run() override;

class IOEvent {
public:
IOEvent(IOObject::WaitableHandle event) : m_event(event) {}
virtual ~IOEvent() {}
virtual void WillPoll() {}
virtual void DidPoll() {}
virtual void Disarm() {}
IOObject::WaitableHandle GetHandle() { return m_event; }

protected:
IOObject::WaitableHandle m_event;
};
using IOEventUP = std::unique_ptr<IOEvent>;

protected:
void UnregisterReadObject(IOObject::WaitableHandle handle) override;

void Interrupt() override;

private:
void ProcessReadObject(IOObject::WaitableHandle handle);
llvm::Expected<size_t> Poll();

struct FdInfo {
void *event;
IOEventUP event;
Callback callback;
};
llvm::DenseMap<IOObject::WaitableHandle, FdInfo> m_read_fds;
Expand Down
7 changes: 4 additions & 3 deletions lldb/include/lldb/Utility/IOObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
#include <sys/types.h>

#include "lldb/lldb-private.h"
#include "lldb/lldb-types.h"

namespace lldb_private {

Expand All @@ -24,9 +25,9 @@ class IOObject {
eFDTypeSocket, // Socket requiring send/recv
};

// TODO: On Windows this should be a HANDLE, and wait should use
// WaitForMultipleObjects
typedef int WaitableHandle;
// A handle for integrating with the host event loop model.
using WaitableHandle = lldb::file_t;

static const WaitableHandle kInvalidHandleValue;

IOObject(FDType type) : m_fd_type(type) {}
Expand Down
4 changes: 4 additions & 0 deletions lldb/source/Host/common/File.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,11 @@ int NativeFile::GetDescriptor() const {
}

IOObject::WaitableHandle NativeFile::GetWaitableHandle() {
#ifdef _WIN32
return (HANDLE)_get_osfhandle(GetDescriptor());
#else
return GetDescriptor();
#endif
}

FILE *NativeFile::GetStream() {
Expand Down
2 changes: 1 addition & 1 deletion lldb/source/Host/common/JSONTransport.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ ReadFull(IOObject &descriptor, size_t length,
if (timeout && timeout_supported) {
SelectHelper sh;
sh.SetTimeout(*timeout);
sh.FDSetRead(descriptor.GetWaitableHandle());
sh.FDSetRead((lldb::socket_t)descriptor.GetWaitableHandle());
Status status = sh.Select();
if (status.Fail()) {
// Convert timeouts into a specific error.
Expand Down
3 changes: 1 addition & 2 deletions lldb/source/Host/common/Socket.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -313,8 +313,7 @@ Socket::DecodeHostAndPort(llvm::StringRef host_and_port) {
}

IOObject::WaitableHandle Socket::GetWaitableHandle() {
// TODO: On Windows, use WSAEventSelect
return m_socket;
return (IOObject::WaitableHandle)m_socket;
}

Status Socket::Read(void *buf, size_t &num_bytes) {
Expand Down
9 changes: 5 additions & 4 deletions lldb/source/Host/posix/ConnectionFileDescriptorPosix.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ size_t ConnectionFileDescriptor::Read(void *dst, size_t dst_len,
"%p ConnectionFileDescriptor::Read() fd = %" PRIu64
", dst = %p, dst_len = %" PRIu64 ") => %" PRIu64 ", error = %s",
static_cast<void *>(this),
static_cast<uint64_t>(m_io_sp->GetWaitableHandle()),
static_cast<file_t>(m_io_sp->GetWaitableHandle()),
Copy link
Member

Choose a reason for hiding this comment

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

Why did you change these from uint64_t to file_t? It's now inconsistent with the format string above, which uses PRIu64.

lldb/source/Host/posix/ConnectionFileDescriptorPosix.cpp:279:15: error: format specifies type 'unsigned long' but the argument has type 'file_t' (aka 'int') [-Werror,-Wformat]

Copy link
Contributor Author

@ashgti ashgti Jul 1, 2025

Choose a reason for hiding this comment

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

I'll fix that, I was thought that lldb::file_t was the more universal type to use for these handles.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like this was already fixed by 98e6d5c

static_cast<void *>(dst), static_cast<uint64_t>(dst_len),
static_cast<uint64_t>(bytes_read), error.AsCString());
}
Expand Down Expand Up @@ -380,7 +380,7 @@ size_t ConnectionFileDescriptor::Write(const void *src, size_t src_len,
"%p ConnectionFileDescriptor::Write(fd = %" PRIu64
", src = %p, src_len = %" PRIu64 ") => %" PRIu64 " (error = %s)",
static_cast<void *>(this),
static_cast<uint64_t>(m_io_sp->GetWaitableHandle()),
static_cast<file_t>(m_io_sp->GetWaitableHandle()),
static_cast<const void *>(src), static_cast<uint64_t>(src_len),
static_cast<uint64_t>(bytes_sent), error.AsCString());
}
Expand Down Expand Up @@ -451,7 +451,8 @@ ConnectionFileDescriptor::BytesAvailable(const Timeout<std::micro> &timeout,
if (timeout)
select_helper.SetTimeout(*timeout);

select_helper.FDSetRead(handle);
// FIXME: Migrate to MainLoop.
select_helper.FDSetRead((lldb::socket_t)handle);
#if defined(_WIN32)
// select() won't accept pipes on Windows. The entire Windows codepath
// needs to be converted over to using WaitForMultipleObjects and event
Expand Down Expand Up @@ -493,7 +494,7 @@ ConnectionFileDescriptor::BytesAvailable(const Timeout<std::micro> &timeout,
break; // Lets keep reading to until we timeout
}
} else {
if (select_helper.FDIsSetRead(handle))
if (select_helper.FDIsSetRead((lldb::socket_t)handle))
return eConnectionStatusSuccess;

if (select_helper.FDIsSetRead(pipe_fd)) {
Expand Down
183 changes: 142 additions & 41 deletions lldb/source/Host/windows/MainLoopWindows.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@

#include "lldb/Host/windows/MainLoopWindows.h"
#include "lldb/Host/Config.h"
#include "lldb/Host/Socket.h"
#include "lldb/Utility/Status.h"
#include "llvm/Config/llvm-config.h"
#include "llvm/Support/Casting.h"
#include "llvm/Support/WindowsError.h"
#include <algorithm>
#include <cassert>
#include <cerrno>
Expand All @@ -31,6 +34,122 @@ static DWORD ToTimeout(std::optional<MainLoopWindows::TimePoint> point) {
return ceil<milliseconds>(dur).count();
}

namespace {

class PipeEvent : public MainLoopWindows::IOEvent {
public:
explicit PipeEvent(HANDLE handle)
: IOEvent((IOObject::WaitableHandle)CreateEventW(
NULL, /*bManualReset=*/FALSE,
/*bInitialState=*/FALSE, NULL)),
m_handle(handle), m_ready(CreateEventW(NULL, /*bManualReset=*/FALSE,
/*bInitialState=*/FALSE, NULL)) {
assert(m_event && m_ready);
}

~PipeEvent() override {
if (m_monitor_thread.joinable()) {
m_stopped = true;
SetEvent(m_ready);
// Keep trying to cancel ReadFile() until the thread exits.
do {
CancelIoEx((HANDLE)m_handle, /*lpOverlapped=*/NULL);
} while (WaitForSingleObject(m_monitor_thread.native_handle(), 1) ==
WAIT_TIMEOUT);
m_monitor_thread.join();
}
CloseHandle((HANDLE)m_event);
CloseHandle(m_ready);
}

void WillPoll() override {
if (!m_monitor_thread.joinable())
Copy link
Collaborator

Choose a reason for hiding this comment

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

What would you say to creating the thread in the constructor? It would remove the joinable() calls, and thing that adding a pipe to the loop is quickly followed by a poll operation anyway.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had this at first but I needed to have an extra ivar for starting the signaling the thread to start in the 'WillPoll', otherwise if you registered a PipeEvent and didn't immediately run the loop it potentially prematurely get a read signal.

I tried using the m_ready variable for triggering that, but it got complicated if an event fired between polling events because of when we call 'Disarm'.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Okay, I see what you mean. Looks good then.

m_monitor_thread = std::thread(&PipeEvent::Monitor, this);
}

void Disarm() override { SetEvent(m_ready); }

/// Monitors the handle performing a zero byte read to determine when data is
/// avaiable.
void Monitor() {
do {
char buf[1];
DWORD bytes_read = 0;
OVERLAPPED ov = {0};
// Block on a 0-byte read; this will only resume when data is
// available in the pipe. The pipe must be PIPE_WAIT or this thread
// will spin.
BOOL success =
ReadFile(m_handle, buf, /*nNumberOfBytesToRead=*/0, &bytes_read, &ov);
DWORD bytes_available = 0;
DWORD err = GetLastError();
if (!success && err == ERROR_IO_PENDING) {
success = GetOverlappedResult(m_handle, &ov, &bytes_read,
/*bWait=*/TRUE);
err = GetLastError();
}
if (success) {
success =
PeekNamedPipe(m_handle, NULL, 0, NULL, &bytes_available, NULL);
err = GetLastError();
}
if (success) {
if (bytes_available == 0) {
// This can happen with a zero-byte write. Try again.
continue;
}
} else if (err == ERROR_NO_DATA) {
// The pipe is nonblocking. Try again.
Sleep(0);
continue;
} else if (err == ERROR_OPERATION_ABORTED) {
// Read may have been cancelled, try again.
continue;
}

SetEvent((HANDLE)m_event);

// Wait until the current read is consumed before doing the next read.
WaitForSingleObject(m_ready, INFINITE);
} while (!m_stopped);
}

private:
HANDLE m_handle;
HANDLE m_ready;
std::thread m_monitor_thread;
std::atomic<bool> m_stopped = false;
};

class SocketEvent : public MainLoopWindows::IOEvent {
public:
explicit SocketEvent(SOCKET socket)
: IOEvent((IOObject::WaitableHandle)WSACreateEvent()), m_socket(socket) {
assert(event != WSA_INVALID_EVENT);
}

~SocketEvent() override { WSACloseEvent((HANDLE)m_event); }

void WillPoll() {
int result = WSAEventSelect(m_socket, (HANDLE)m_event,
FD_READ | FD_ACCEPT | FD_CLOSE);
assert(result == 0);
UNUSED_IF_ASSERT_DISABLED(result);
}

void DidPoll() {
int result = WSAEventSelect(m_socket, WSA_INVALID_EVENT, 0);
assert(result == 0);
UNUSED_IF_ASSERT_DISABLED(result);
}

void Disarm() override { WSAResetEvent((HANDLE)m_event); }

SOCKET m_socket;
};

} // namespace

MainLoopWindows::MainLoopWindows() {
m_interrupt_event = WSACreateEvent();
assert(m_interrupt_event != WSA_INVALID_EVENT);
Expand All @@ -44,26 +163,20 @@ MainLoopWindows::~MainLoopWindows() {
}

llvm::Expected<size_t> MainLoopWindows::Poll() {
std::vector<WSAEVENT> events;
std::vector<HANDLE> events;
events.reserve(m_read_fds.size() + 1);
for (auto &[fd, info] : m_read_fds) {
int result = WSAEventSelect(fd, info.event, FD_READ | FD_ACCEPT | FD_CLOSE);
assert(result == 0);
UNUSED_IF_ASSERT_DISABLED(result);

events.push_back(info.event);
for (auto &[_, fd_info] : m_read_fds) {
fd_info.event->WillPoll();
events.push_back((HANDLE)fd_info.event->GetHandle());
}
events.push_back(m_interrupt_event);

DWORD result =
WSAWaitForMultipleEvents(events.size(), events.data(), FALSE,
ToTimeout(GetNextWakeupTime()), FALSE);

for (auto &fd : m_read_fds) {
int result = WSAEventSelect(fd.first, WSA_INVALID_EVENT, 0);
assert(result == 0);
UNUSED_IF_ASSERT_DISABLED(result);
}
for (auto &[_, fd_info] : m_read_fds)
fd_info.event->DidPoll();

if (result >= WSA_WAIT_EVENT_0 && result < WSA_WAIT_EVENT_0 + events.size())
return result - WSA_WAIT_EVENT_0;
Expand All @@ -83,28 +196,25 @@ MainLoopWindows::RegisterReadObject(const IOObjectSP &object_sp,
error = Status::FromErrorString("IO object is not valid.");
return nullptr;
}
if (object_sp->GetFdType() != IOObject::eFDTypeSocket) {
error = Status::FromErrorString(
"MainLoopWindows: non-socket types unsupported on Windows");
return nullptr;
}

WSAEVENT event = WSACreateEvent();
if (event == WSA_INVALID_EVENT) {
error =
Status::FromErrorStringWithFormat("Cannot create monitoring event.");
IOObject::WaitableHandle waitable_handle = object_sp->GetWaitableHandle();
assert(waitable_handle != IOObject::kInvalidHandleValue);

if (m_read_fds.find(waitable_handle) != m_read_fds.end()) {
error = Status::FromErrorStringWithFormat(
"File descriptor %d already monitored.", waitable_handle);
return nullptr;
}

const bool inserted =
m_read_fds
.try_emplace(object_sp->GetWaitableHandle(), FdInfo{event, callback})
.second;
if (!inserted) {
WSACloseEvent(event);
error = Status::FromErrorStringWithFormat(
"File descriptor %d already monitored.",
object_sp->GetWaitableHandle());
if (object_sp->GetFdType() == IOObject::eFDTypeSocket)
m_read_fds[waitable_handle] = {
std::make_unique<SocketEvent>((SOCKET)waitable_handle), callback};
else if (GetFileType(waitable_handle) == FILE_TYPE_PIPE)
m_read_fds[waitable_handle] = {
std::make_unique<PipeEvent>((HANDLE)waitable_handle), callback};
else {
error = Status::FromErrorStringWithFormat("Unsupported file type %d",
GetFileType(waitable_handle));
return nullptr;
}

Expand All @@ -114,18 +224,9 @@ MainLoopWindows::RegisterReadObject(const IOObjectSP &object_sp,
void MainLoopWindows::UnregisterReadObject(IOObject::WaitableHandle handle) {
auto it = m_read_fds.find(handle);
assert(it != m_read_fds.end());
BOOL result = WSACloseEvent(it->second.event);
assert(result == TRUE);
UNUSED_IF_ASSERT_DISABLED(result);
m_read_fds.erase(it);
}

void MainLoopWindows::ProcessReadObject(IOObject::WaitableHandle handle) {
auto it = m_read_fds.find(handle);
if (it != m_read_fds.end())
it->second.callback(*this); // Do the work
}

Status MainLoopWindows::Run() {
m_terminate_request = false;

Expand All @@ -138,8 +239,8 @@ Status MainLoopWindows::Run() {

if (*signaled_event < m_read_fds.size()) {
auto &KV = *std::next(m_read_fds.begin(), *signaled_event);
WSAResetEvent(KV.second.event);
ProcessReadObject(KV.first);
KV.second.event->Disarm();
KV.second.callback(*this); // Do the work.
} else {
assert(*signaled_event == m_read_fds.size());
WSAResetEvent(m_interrupt_event);
Expand Down
9 changes: 9 additions & 0 deletions lldb/source/Utility/IOObject.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,16 @@

#include "lldb/Utility/IOObject.h"

#ifdef _WIN32
#include "lldb/Host/windows/windows.h"
#endif

using namespace lldb_private;

#ifdef _WIN32
const IOObject::WaitableHandle IOObject::kInvalidHandleValue =
INVALID_HANDLE_VALUE;
#else
const IOObject::WaitableHandle IOObject::kInvalidHandleValue = -1;
#endif
IOObject::~IOObject() = default;
Loading
Loading