Skip to content

[lldb][Windows] Support OutputDebugString#196395

Merged
Nerixyz merged 7 commits into
llvm:mainfrom
Nerixyz:feat/win-output-debug-string
Jun 2, 2026
Merged

[lldb][Windows] Support OutputDebugString#196395
Nerixyz merged 7 commits into
llvm:mainfrom
Nerixyz:feat/win-output-debug-string

Conversation

@Nerixyz
Copy link
Copy Markdown
Contributor

@Nerixyz Nerixyz commented May 7, 2026

This picks https://reviews.llvm.org/D128541 back up. It implements support for OutputDebugStringA/W on Windows. It's used by some logging systems.

The main changes since the original patch:

  • Use WaitForDebugEventEx for getting debug events over WaitForDebugEvent. According to the docs, the only difference is that the -Ex version correctly outputs Unicode strings.
  • Support strings longer than 64 KiB. I set an arbitrary limit of 1 MiB for the strings we read. The debugger interface only tells us the length modulo 64 KiB which is a bit awkward. Libraries like Qt already chunk calls to OutputDebugString, so strings shouldn't be too big in practice.
  • Output to stdout instead of a log channel, so the output is always enabled. I don't know if this should go to stdout or stderr.

Tested that it works with lldb-dap and shows up in the debug console in all modes.

Closes #185891.

@llvmorg-github-actions
Copy link
Copy Markdown

@llvm/pr-subscribers-lldb

Author: Nerixyz (Nerixyz)

Changes

This picks https://reviews.llvm.org/D128541 back up. It implements support for OutputDebugStringA/W on Windows. It's used by some logging systems.

The main changes since the original patch:

  • Use WaitForDebugEventEx for getting debug events over WaitForDebugEvent. According to the docs, the only difference is that the -Ex version correctly outputs Unicode strings.
  • Support strings longer than 64 KiB. I set an arbitrary limit of 1 MiB for the strings we read. The debugger interface only tells us the length modulo 64 KiB which is a bit awkward. Libraries like Qt already chunk calls to OutputDebugString, so strings shouldn't be too big in practice.
  • Output to stdout instead of a log channel, so the output is always enabled. I don't know if this should go to stdout or stderr.

Tested that it works with lldb-dap and shows up in the debug console in all modes.

Closes #185891.


Full diff: https://github.com/llvm/llvm-project/pull/196395.diff

10 Files Affected:

  • (modified) lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp (+11-8)
  • (modified) lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h (+2-1)
  • (modified) lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp (+4-2)
  • (modified) lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h (+2-1)
  • (modified) lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h (+3-2)
  • (modified) lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp (+5-1)
  • (modified) lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h (+2-1)
  • (modified) lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp (+90-1)
  • (modified) lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h (+6-1)
  • (added) lldb/test/Shell/Process/Windows/output_debug_string.cpp (+57)
diff --git a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
index 6359a63dfef91..15cba5d2b51d2 100644
--- a/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp
@@ -173,7 +173,7 @@ Status DebuggerThread::StopDebugging(bool terminate) {
   // If we're stuck waiting for an exception to continue (e.g. the user is at a
   // breakpoint messing around in the debugger), continue it now.  But only
   // AFTER calling TerminateProcess to make sure that the very next call to
-  // WaitForDebugEvent is an exit process event.
+  // WaitForDebugEventEx is an exit process event.
   if (m_active_exception.get()) {
     LLDB_LOG(log, "masking active exception");
     ContinueAsyncException(ExceptionResult::MaskException);
@@ -233,10 +233,10 @@ void DebuggerThread::DebugLoop() {
   Log *log = GetLog(WindowsLog::Event);
   DEBUG_EVENT dbe = {};
   bool should_debug = true;
-  LLDB_LOG_VERBOSE(log, "Entering WaitForDebugEvent loop");
+  LLDB_LOG_VERBOSE(log, "Entering WaitForDebugEventEx loop");
   while (should_debug) {
-    LLDB_LOG_VERBOSE(log, "Calling WaitForDebugEvent");
-    BOOL wait_result = WaitForDebugEvent(&dbe, INFINITE);
+    LLDB_LOG_VERBOSE(log, "Calling WaitForDebugEventEx");
+    BOOL wait_result = WaitForDebugEventEx(&dbe, INFINITE);
     if (wait_result) {
       DWORD continue_status = DBG_CONTINUE;
       bool shutting_down = m_is_shutting_down;
@@ -312,9 +312,9 @@ void DebuggerThread::DebugLoop() {
           // detaching with leaving breakpoint exception event on the queue may
           // cause target process to crash so process events as possible since
           // target threads are running at this time, there is possibility to
-          // have some breakpoint exception between last WaitForDebugEvent and
+          // have some breakpoint exception between last WaitForDebugEventEx and
           // DebugActiveProcessStop but ignore for now.
-          while (WaitForDebugEvent(&dbe, 0)) {
+          while (WaitForDebugEventEx(&dbe, 0)) {
             continue_status = DBG_CONTINUE;
             if (dbe.dwDebugEventCode == EXCEPTION_DEBUG_EVENT &&
                 !(dbe.u.Exception.ExceptionRecord.ExceptionCode ==
@@ -337,7 +337,7 @@ void DebuggerThread::DebugLoop() {
         should_debug = false;
       }
     } else {
-      LLDB_LOG(log, "returned FALSE from WaitForDebugEvent.  Error = {0}",
+      LLDB_LOG(log, "returned FALSE from WaitForDebugEventEx.  Error = {0}",
                ::GetLastError());
 
       should_debug = false;
@@ -345,7 +345,7 @@ void DebuggerThread::DebugLoop() {
   }
   FreeProcessHandles();
 
-  LLDB_LOG(log, "WaitForDebugEvent loop completed, exiting.");
+  LLDB_LOG(log, "WaitForDebugEventEx loop completed, exiting.");
   ::SetEvent(m_debugging_ended_event);
 }
 
@@ -567,6 +567,9 @@ DebuggerThread::HandleUnloadDllEvent(const UNLOAD_DLL_DEBUG_INFO &info,
 DWORD
 DebuggerThread::HandleODSEvent(const OUTPUT_DEBUG_STRING_INFO &info,
                                DWORD thread_id) {
+  m_debug_delegate->OnDebugString(
+      llvm::bit_cast<lldb::addr_t>(info.lpDebugStringData),
+      info.fUnicode == TRUE, info.nDebugStringLength);
   return DBG_CONTINUE;
 }
 
diff --git a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
index aff2dd610eea0..842fa4a6464b5 100644
--- a/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/IDebugDelegate.h
@@ -35,7 +35,8 @@ class IDebugDelegate {
   virtual void OnLoadDll(const ModuleSpec &module_spec,
                          lldb::addr_t module_addr) = 0;
   virtual void OnUnloadDll(lldb::addr_t module_addr) = 0;
-  virtual void OnDebugString(const std::string &string) = 0;
+  virtual void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+                             uint16_t length_lower_word) = 0;
   virtual void OnDebuggerError(const Status &error, uint32_t type) = 0;
 };
 }
diff --git a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
index dfa1d76d35fa0..647502ddaac25 100644
--- a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.cpp
@@ -56,9 +56,11 @@ void LocalDebugDelegate::OnUnloadDll(lldb::addr_t module_addr) {
     process->OnUnloadDll(module_addr);
 }
 
-void LocalDebugDelegate::OnDebugString(const std::string &string) {
+void LocalDebugDelegate::OnDebugString(lldb::addr_t debug_string_addr,
+                                       bool is_unicode,
+                                       uint16_t length_lower_word) {
   if (ProcessWindowsSP process = GetProcessPointer())
-    process->OnDebugString(string);
+    process->OnDebugString(debug_string_addr, is_unicode, length_lower_word);
 }
 
 void LocalDebugDelegate::OnDebuggerError(const Status &error, uint32_t type) {
diff --git a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
index 7494dbbb0cfb6..d602771b3a7a4 100644
--- a/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
+++ b/lldb/source/Plugins/Process/Windows/Common/LocalDebugDelegate.h
@@ -51,7 +51,8 @@ class LocalDebugDelegate : public IDebugDelegate {
   void OnLoadDll(const lldb_private::ModuleSpec &module_spec,
                  lldb::addr_t module_addr) override;
   void OnUnloadDll(lldb::addr_t module_addr) override;
-  void OnDebugString(const std::string &message) override;
+  void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+                     uint16_t length_lower_word) override;
   void OnDebuggerError(const Status &error, uint32_t type) override;
 
 private:
diff --git a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
index cfba787a3d220..75add16ce6099 100644
--- a/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/NativeProcessWindows.h
@@ -173,8 +173,9 @@ class NativeDebugDelegate : public IDebugDelegate {
     m_process.OnUnloadDll(module_addr);
   }
 
-  void OnDebugString(const std::string &string) override {
-    m_process.OnDebugString(string);
+  void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+                     uint16_t length_lower_word) override {
+    m_process.OnDebugString(debug_string_addr, is_unicode, length_lower_word);
   }
 
   void OnDebuggerError(const Status &error, uint32_t type) override {
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
index 51b2dfcb74d86..ac89f146e2847 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.cpp
@@ -543,7 +543,11 @@ void ProcessDebugger::OnUnloadDll(lldb::addr_t module_addr) {
   // Do nothing by default
 }
 
-void ProcessDebugger::OnDebugString(const std::string &string) {}
+void ProcessDebugger::OnDebugString(lldb::addr_t debug_string_addr,
+                                    bool is_unicode,
+                                    uint16_t length_lower_word) {
+  // Do nothing by default
+}
 
 void ProcessDebugger::OnDebuggerError(const Status &error, uint32_t type) {
   llvm::sys::ScopedLock lock(m_mutex);
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
index a4db76455ef21..25c877d5b4f17 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessDebugger.h
@@ -59,7 +59,8 @@ class ProcessDebugger {
   virtual void OnLoadDll(const ModuleSpec &module_spec,
                          lldb::addr_t module_addr);
   virtual void OnUnloadDll(lldb::addr_t module_addr);
-  virtual void OnDebugString(const std::string &string);
+  virtual void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+                             uint16_t length_lower_word);
   virtual void OnDebuggerError(const Status &error, uint32_t type);
 
 protected:
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
index 28b5554069c90..9d3c2e8e31ba4 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp
@@ -36,6 +36,7 @@
 #include "lldb/Utility/State.h"
 
 #include "llvm/Support/ConvertUTF.h"
+#include "llvm/Support/ErrorExtras.h"
 #include "llvm/Support/Format.h"
 #include "llvm/Support/Threading.h"
 #include "llvm/Support/raw_ostream.h"
@@ -870,7 +871,95 @@ void ProcessWindows::OnUnloadDll(lldb::addr_t module_addr) {
     dyld->OnUnloadModule(module_addr);
 }
 
-void ProcessWindows::OnDebugString(const std::string &string) {}
+void ProcessWindows::OnDebugString(lldb::addr_t debug_string_addr,
+                                   bool is_unicode,
+                                   uint16_t length_lower_word) {
+  Log *log = GetLog(WindowsLog::Process);
+
+  llvm::SmallVector<char, 256> buffer;
+  llvm::Error err =
+      ReadDebugString(debug_string_addr, is_unicode, length_lower_word, buffer);
+  if (err) {
+    LLDB_LOG_ERROR(log, std::move(err),
+                   "Failed to read debug string at {1:x} (size & 0xffff={2}, "
+                   "unicode={3}): {0}",
+                   debug_string_addr, length_lower_word, is_unicode);
+    return;
+  }
+  if (buffer.empty())
+    return;
+
+  if (is_unicode) {
+    if (buffer.size() % 2 != 0) {
+      LLDB_LOG(log, "Read debug string had uneven size: {0}", buffer.size());
+      return;
+    }
+    llvm::ArrayRef<unsigned short> utf16(
+        reinterpret_cast<const unsigned short *>(buffer.data()),
+        buffer.size() / 2);
+    std::string out;
+    if (!llvm::convertUTF16ToUTF8String(utf16, out)) {
+      LLDB_LOG(log, "Debug string is not valid Utf 16");
+      return;
+    }
+
+    AppendSTDOUT(out.data(), out.size());
+  } else {
+    AppendSTDOUT(buffer.data(), buffer.size());
+  }
+}
+
+llvm::Error
+ProcessWindows::ReadDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+                                uint16_t length_lower_word,
+                                llvm::SmallVectorImpl<char> &output) {
+  if (is_unicode && length_lower_word % 2 != 0) {
+    return llvm::createStringError(
+        "Utf16 string can't have uneven size in bytes");
+  }
+
+  const auto is_zero_terminated = [&] {
+    // The zero terminator is always at the end of the buffer.
+    if (is_unicode) {
+      return output.size() >= 2 && output.back() == 0 &&
+             output[output.size() - 2] == 0;
+    }
+    return !output.empty() && output.back() == 0;
+  };
+
+  // Read at most 1 MiB ((1 << 16) * 16 - 1 Bytes) since we don't know the exact
+  // size of the string. We know that `strlen(string) & 0xffff ==
+  // length_lower_word`, so we read in chunks until we reach the terminator:
+  // - 0: `length_lower_word` Bytes
+  // - 1..16: 64 KiB (= 2^16 Bytes)
+  size_t start = length_lower_word == 0 ? 1 : 0;
+  for (size_t i = start; i < 16; ++i) {
+    output.resize_for_overwrite(length_lower_word + i * (1 << 16));
+    size_t chunk_size = i == 0 ? length_lower_word : (1 << 16);
+    lldb::addr_t addr = debug_string_addr + output.size_in_bytes() - chunk_size;
+
+    Status error;
+    size_t bytes_read =
+        DoReadMemory(addr, output.end() - chunk_size, chunk_size, error);
+    if (error.Fail())
+      return error.takeError();
+
+    if (bytes_read != chunk_size) {
+      return llvm::createStringErrorV(
+          "Expected to read {0} bytes, but read {1}", chunk_size, bytes_read);
+    }
+
+    if (is_zero_terminated())
+      break;
+  }
+
+  if (!is_zero_terminated())
+    return llvm::createStringError("String is 1 MiB or larger");
+
+  // Remove null terminator.
+  output.pop_back_n(is_unicode ? 2 : 1);
+  return llvm::Error::success();
+}
 
 void ProcessWindows::OnDebuggerError(const Status &error, uint32_t type) {
   llvm::sys::ScopedLock lock(m_mutex);
diff --git a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
index 228619d0e3d5e..58dc510a17850 100644
--- a/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
+++ b/lldb/source/Plugins/Process/Windows/Common/ProcessWindows.h
@@ -91,7 +91,8 @@ class ProcessWindows : public Process, public ProcessDebugger {
   void OnLoadDll(const ModuleSpec &module_spec,
                  lldb::addr_t module_addr) override;
   void OnUnloadDll(lldb::addr_t module_addr) override;
-  void OnDebugString(const std::string &string) override;
+  void OnDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+                     uint16_t length_lower_word) override;
   void OnDebuggerError(const Status &error, uint32_t type) override;
 
   std::optional<uint32_t> GetWatchpointSlotCount() override;
@@ -114,6 +115,10 @@ class ProcessWindows : public Process, public ProcessDebugger {
                                MemoryRegionInfo &info) override;
 
 private:
+  llvm::Error ReadDebugString(lldb::addr_t debug_string_addr, bool is_unicode,
+                              uint16_t length_lower_word,
+                              llvm::SmallVectorImpl<char> &output);
+
   struct WatchpointInfo {
     uint32_t slot_id;
     lldb::addr_t address;
diff --git a/lldb/test/Shell/Process/Windows/output_debug_string.cpp b/lldb/test/Shell/Process/Windows/output_debug_string.cpp
new file mode 100644
index 0000000000000..231d529e30a84
--- /dev/null
+++ b/lldb/test/Shell/Process/Windows/output_debug_string.cpp
@@ -0,0 +1,57 @@
+// REQUIRES: target-windows
+// RUN: %build --compiler=clang-cl -o %t.exe -- %s
+// RUN: %lldb -f %t.exe -o "log enable windows process" -o r -o q | FileCheck %s
+
+#include <Windows.h>
+#include <string>
+
+int main() {
+  OutputDebugStringA("My string\nnext line\nBut this doesn't have trailing newline|");
+  OutputDebugStringW(L"OutputDebugStringW with some emojis 🦎🐊🐢🐍\n");
+  OutputDebugStringW(L"Another W1\n");
+  OutputDebugStringW(L"Another W2\n");
+  OutputDebugStringA("Some A1\n");
+  OutputDebugStringA("Some A2\n");
+  OutputDebugStringA("Some A3\n");
+
+  std::wstring maxW((1 << 19) - 4, L'A');
+  maxW.push_back(L'B');
+  maxW.push_back(L'\n');
+  OutputDebugStringW(maxW.c_str());
+
+  Sleep(100);
+
+  std::string maxA((1 << 20) - 4, 'C');
+  maxA.push_back(L'D');
+  maxA.push_back(L'\n');
+  OutputDebugStringA(maxA.c_str());
+
+  Sleep(100);
+
+  std::wstring tooBigW((1 << 19) - 3, L'E');
+  tooBigW.push_back(L'F');
+  tooBigW.push_back(L'\n');
+  OutputDebugStringW(tooBigW.c_str());
+
+  Sleep(100);
+
+  std::string tooBigA((1 << 20) - 3, 'G');
+  tooBigA.push_back(L'H');
+  tooBigA.push_back(L'\n');
+  OutputDebugStringA(tooBigA.c_str());
+
+  return 0;
+}
+
+// CHECK: My string
+// CHECK-NEXT: next line
+// CHECK-NEXT: But this doesn't have trailing newline|OutputDebugStringW with some emojis 🦎🐊🐢🐍
+// CHECK-NEXT: Another W1
+// CHECK-NEXT: Another W2
+// CHECK-NEXT: Some A1
+// CHECK-NEXT: Some A2
+// CHECK-NEXT: Some A3
+// CHECK-NEXT: {{A+B}}
+// CHECK-NEXT: {{C+D}}
+// CHECK-NEXT: Failed to read debug string at 0x{{.*}} (size & 0xffff=0, unicode=true): String is 1 MiB or larger
+// CHECK-NEXT: Failed to read debug string at 0x{{.*}} (size & 0xffff=0, unicode=false): String is 1 MiB or larger

Comment thread lldb/source/Plugins/Process/Windows/Common/ProcessWindows.cpp Outdated
maxW.push_back(L'\n');
OutputDebugStringW(maxW.c_str());

Sleep(100);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What is the purpose of the sleep?

I vaguely recall something about the buffer for these messages being finite, so is it to allow time for lldb to read the messages before the next ones overwrite them?

Copy link
Copy Markdown
Contributor Author

@Nerixyz Nerixyz May 8, 2026

Choose a reason for hiding this comment

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

This particular sleep shouldn't be strictly necessary. However, the sleeps afterward are necessary to get LLDB to print the debug string before it logs the error about the large strings. I added a comment about that now.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could you explain how the timing works? I suppose there is some delay between the program calling OutputDebugStringW and LLDB picking up the debug event?

So if you have:

OutputDebugStringW(message A);
A:
OutputDebugStringW(message B);
B:

If LLDB decides to read messages at A, it will only see message A, but if the event comes in later it will see message A and message B. The timing of the event is up to Windows so by waiting we expect that it will generate the event in that time.

Would it be possible to do this without the sleeps? If you were to CHECK instead of CHECK-NEXT. What do you lose if you are not so strict?

Though if the larger messages push the others out of the limited message buffer (if it is in fact limited) then this idea does not work. You have to read out all the small messages as you currently do, then send the large ones.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I think the timing difference comes from logs directly writing to the output and AppendSTDOUT only sending an event to the Debugger (which eventually consumes it and prints). We log in the event thread (where we call WaitForDebugEventEx) and the Debugger listens in a different thread.

So if we do

OutputDebugString(messageA);
OutputDebugString(messageTooLong);

We either end up with

A
error: ...
OR
error: ...
A

because if the calls are immediately after each other, we might report the error before the Debugger wakes up.

If LLDB decides to read messages at A, it will only see message A, but if the event comes in later it will see message A and message B. The timing of the event is up to Windows so by waiting we expect that it will generate the event in that time.

We still get two separate events for these calls and, in the event thread, these are handled in order.

Would it be possible to do this without the sleeps? If you were to CHECK instead of CHECK-NEXT. What do you lose if you are not so strict?

I don't think that will work, as I need to check for both orders above. Could use CHECK-DAG, but that wouldn't ensure the relative order of debug message. What would work is using two FileCheck invocations. One to check for the output itself and one to check for errors.

Comment on lines +916 to +919
if (is_unicode && length_lower_word % 2 != 0) {
return llvm::createStringError(
"Utf16 string can't have uneven size in bytes");
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (is_unicode && length_lower_word % 2 != 0) {
return llvm::createStringError(
"Utf16 string can't have uneven size in bytes");
}
if (is_unicode && length_lower_word % 2 != 0)
return llvm::createStringError(
"Utf16 string can't have uneven size in bytes");

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Depends on the outcome of https://discourse.llvm.org/t/rfc-clarify-brace-omission-for-wrapped-single-statement-bodies/90739 I suppose. Though as far as I know, in LLDB, we already went with the "omit if one line".

Comment on lines +923 to +926
if (is_unicode) {
return output.size() >= 2 && output.back() == 0 &&
output[output.size() - 2] == 0;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
if (is_unicode) {
return output.size() >= 2 && output.back() == 0 &&
output[output.size() - 2] == 0;
}
if (is_unicode)
return output.size() >= 2 && output.back() == 0 &&
output[output.size() - 2] == 0;

Comment thread lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp Outdated
Comment thread lldb/source/Plugins/Process/Windows/Common/DebuggerThread.cpp Outdated
return;

if (is_unicode) {
if (buffer.size() % 2 != 0) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IMO, we can assume that the Windows API returns a proper Unicode string with an even number of bytes. Maybe this could become an assert instead?

@DavidSpickett
Copy link
Copy Markdown
Contributor

Please add a release note for this feature. The key thing is where users can expect to see the message, when using the command line or lldb-dap.

Nerixyz added a commit that referenced this pull request May 11, 2026
This makes use of
[`WaitForDebugEventEx`](https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-waitfordebugeventex)
over `WaitForDebugEvent` if available (Windows 10+).

The two functions are identical except for the handling of
`OutputDebugStringW`. The `-Ex` version forwards the string as Unicode
whereas the other version forwards ASCII strings. Since we don't handle
these outputs yet, it shouldn't make any difference.

Split from #196395.
EuphoricThinking pushed a commit to EuphoricThinking/llvm-project that referenced this pull request May 14, 2026
This makes use of
[`WaitForDebugEventEx`](https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-waitfordebugeventex)
over `WaitForDebugEvent` if available (Windows 10+).

The two functions are identical except for the handling of
`OutputDebugStringW`. The `-Ex` version forwards the string as Unicode
whereas the other version forwards ASCII strings. Since we don't handle
these outputs yet, it shouldn't make any difference.

Split from llvm#196395.
pedroMVicente pushed a commit to pedroMVicente/llvm-project that referenced this pull request May 19, 2026
This makes use of
[`WaitForDebugEventEx`](https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-waitfordebugeventex)
over `WaitForDebugEvent` if available (Windows 10+).

The two functions are identical except for the handling of
`OutputDebugStringW`. The `-Ex` version forwards the string as Unicode
whereas the other version forwards ASCII strings. Since we don't handle
these outputs yet, it shouldn't make any difference.

Split from llvm#196395.
Copy link
Copy Markdown
Contributor

@charles-zablit charles-zablit left a comment

Choose a reason for hiding this comment

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

@Nerixyz
Copy link
Copy Markdown
Contributor Author

Nerixyz commented Jun 2, 2026

This does not have to be done in this PR, but we could switch to using main/lldb/include/lldb/Host/windows/LazyImport.h.

Oh yeah, I'll do that in a followup.

@Nerixyz Nerixyz merged commit 2a7524b into llvm:main Jun 2, 2026
11 checks passed
Nerixyz added a commit that referenced this pull request Jun 2, 2026
From
#196395 (review).

This uses `LazyImport` for `WaitForDebugEventEx` to simplify the import.
llvm-upstreamsync Bot pushed a commit to qualcomm/cpullvm-toolchain that referenced this pull request Jun 2, 2026
…01118)

From
llvm/llvm-project#196395 (review).

This uses `LazyImport` for `WaitForDebugEventEx` to simplify the import.
llvm-sync Bot pushed a commit to arm/arm-toolchain that referenced this pull request Jun 2, 2026
…01118)

From
llvm/llvm-project#196395 (review).

This uses `LazyImport` for `WaitForDebugEventEx` to simplify the import.
yingopq pushed a commit to yingopq/llvm-project that referenced this pull request Jun 5, 2026
This picks https://reviews.llvm.org/D128541 back up. It implements
support for `OutputDebugStringA/W` on Windows. It's used by some logging
systems.

The main changes since the original patch:
- Use `WaitForDebugEventEx` for getting debug events over
`WaitForDebugEvent`. According to the
[docs](https://learn.microsoft.com/en-us/windows/win32/api/debugapi/nf-debugapi-waitfordebugeventex),
the only difference is that the -Ex version correctly outputs Unicode
strings.
- Support strings longer than 64 KiB. I set an arbitrary limit of 1 MiB
for the strings we read. The debugger interface only tells us the length
modulo 64 KiB which is a bit awkward. Libraries like Qt already chunk
calls to `OutputDebugString`, so strings shouldn't be too big in
practice.
- Output to stdout instead of a log channel, so the output is always
enabled. I don't know if this should go to stdout or stderr.

Tested that it works with lldb-dap and shows up in the debug console in
all modes.

Closes llvm#185891.

---------

Co-authored-by: Alvin Wong <[email protected]>
yingopq pushed a commit to yingopq/llvm-project that referenced this pull request Jun 5, 2026
From
llvm#196395 (review).

This uses `LazyImport` for `WaitForDebugEventEx` to simplify the import.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[LLDB] Feature request: OutputDebugString[A|W] support

3 participants