https://github.com/charles-zablit updated 
https://github.com/llvm/llvm-project/pull/196293

>From aef7024f690f9e458c4493cad425420ad4a41a44 Mon Sep 17 00:00:00 2001
From: Charles Zablit <[email protected]>
Date: Thu, 7 May 2026 13:05:09 +0100
Subject: [PATCH 1/2] [lldb] split NativeFile in platform specific
 implementations

---
 lldb/include/lldb/Host/File.h                | 460 +------------------
 lldb/include/lldb/Host/FileBase.h            | 245 ++++++++++
 lldb/include/lldb/Host/posix/FilePosix.h     |  49 ++
 lldb/include/lldb/Host/windows/FileWindows.h |  65 +++
 lldb/source/Host/CMakeLists.txt              |   2 +
 lldb/source/Host/common/File.cpp             | 348 +++-----------
 lldb/source/Host/posix/FilePosix.cpp         | 220 +++++++++
 lldb/source/Host/windows/FileWindows.cpp     | 139 ++++++
 8 files changed, 790 insertions(+), 738 deletions(-)
 create mode 100644 lldb/include/lldb/Host/FileBase.h
 create mode 100644 lldb/include/lldb/Host/posix/FilePosix.h
 create mode 100644 lldb/include/lldb/Host/windows/FileWindows.h
 create mode 100644 lldb/source/Host/posix/FilePosix.cpp
 create mode 100644 lldb/source/Host/windows/FileWindows.cpp

diff --git a/lldb/include/lldb/Host/File.h b/lldb/include/lldb/Host/File.h
index 590c9fa523b29..3e15961a5c239 100644
--- a/lldb/include/lldb/Host/File.h
+++ b/lldb/include/lldb/Host/File.h
@@ -1,4 +1,4 @@
-//===-- File.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.
@@ -9,457 +9,25 @@
 #ifndef LLDB_HOST_FILE_H
 #define LLDB_HOST_FILE_H
 
-#include "lldb/Host/PosixApi.h"
+#include "lldb/Host/FileBase.h"
 #include "lldb/Host/Terminal.h"
-#include "lldb/Utility/IOObject.h"
-#include "lldb/Utility/Status.h"
-#include "lldb/lldb-private.h"
-#include "llvm/ADT/BitmaskEnum.h"
 
-#include <cstdarg>
-#include <cstdio>
-#include <mutex>
+#include <memory>
 #include <optional>
-#include <sys/types.h>
-
-namespace lldb_private {
-
-LLVM_ENABLE_BITMASK_ENUMS_IN_NAMESPACE();
-
-/// \class File File.h "lldb/Host/File.h"
-/// An abstract base class for files.
-///
-/// Files will often be NativeFiles, which provides a wrapper
-/// around host OS file functionality.   But it
-/// is also possible to subclass file to provide objects that have file
-/// or stream functionality but are not backed by any host OS file.
-class File : public IOObject {
-public:
-  static int kInvalidDescriptor;
-  static FILE *kInvalidStream;
-
-  // NB this enum is used in the lldb platform gdb-remote packet
-  // vFile:open: and existing values cannot be modified.
-  //
-  // The first set of values is defined by gdb headers and can be found
-  // in the documentation at:
-  // * https://sourceware.org/gdb/onlinedocs/gdb/Open-Flags.html#Open-Flags
-  //
-  // The second half are LLDB extensions and use the highest uint32_t bits
-  // to avoid risk of collisions with future gdb remote protocol changes.
-  enum OpenOptions : uint32_t {
-    eOpenOptionReadOnly = 0x0,  // Open file for reading (only)
-    eOpenOptionWriteOnly = 0x1, // Open file for writing (only)
-    eOpenOptionReadWrite = 0x2, // Open file for both reading and writing
-    eOpenOptionAppend =
-        0x8, // Don't truncate file when opening, append to end of file
-    eOpenOptionCanCreate = 0x200, // Create file if doesn't already exist
-    eOpenOptionTruncate = 0x400,  // Truncate file when opening
-    eOpenOptionCanCreateNewOnly =
-        0x800, // Can create file only if it doesn't already exist
-
-    eOpenOptionNonBlocking = (1u << 28), // File reads
-    eOpenOptionDontFollowSymlinks = (1u << 29),
-    eOpenOptionCloseOnExec =
-        (1u << 30), // Close the file when executing a new process
-    eOpenOptionInvalid = (1u << 31), // Used as invalid value
-    LLVM_MARK_AS_BITMASK_ENUM(/* largest_value= */ eOpenOptionInvalid)
-  };
-
-  static constexpr OpenOptions OpenOptionsModeMask =
-      eOpenOptionReadOnly | eOpenOptionWriteOnly | eOpenOptionReadWrite;
-
-  static mode_t ConvertOpenOptionsForPOSIXOpen(OpenOptions open_options);
-  static llvm::Expected<OpenOptions> GetOptionsFromMode(llvm::StringRef mode);
-  static bool DescriptorIsValid(int descriptor) { return descriptor >= 0; };
-  static llvm::Expected<const char *>
-  GetStreamOpenModeFromOptions(OpenOptions options);
-
-  File() : IOObject(eFDTypeFile){};
-
-  /// Read bytes from a file from the current file position into buf.
-  ///
-  /// NOTE: This function is NOT thread safe. Use the read function
-  /// that takes an "off_t &offset" to ensure correct operation in multi-
-  /// threaded environments.
-  ///
-  /// \param[in,out] num_bytes
-  ///    Pass in the size of buf.  Read will pass out the number
-  ///    of bytes read.   Zero bytes read with no error indicates
-  ///    EOF.
-  ///
-  /// \return
-  ///    success, ENOTSUP, or another error.
-  Status Read(void *buf, size_t &num_bytes) override;
-
-  /// Write bytes from buf to a file at the current file position.
-  ///
-  /// NOTE: This function is NOT thread safe. Use the write function
-  /// that takes an "off_t &offset" to ensure correct operation in multi-
-  /// threaded environments.
-  ///
-  /// \param[in,out] num_bytes
-  ///    Pass in the size of buf.  Write will pass out the number
-  ///    of bytes written.   Write will attempt write the full number
-  ///    of bytes and will not return early except on error.
-  ///
-  /// \return
-  ///    success, ENOTSUP, or another error.
-  Status Write(const void *buf, size_t &num_bytes) override;
-
-  /// IsValid
-  ///
-  /// \return
-  ///    true iff the file is valid.
-  bool IsValid() const override;
-
-  /// Flush any buffers and release any resources owned by the file.
-  /// After Close() the file will be invalid.
-  ///
-  /// \return
-  ///     success or an error.
-  Status Close() override;
-
-  /// Get a handle that can be used for OS polling interfaces, such
-  /// as WaitForMultipleObjects, select, or epoll.   This may return
-  /// IOObject::kInvalidHandleValue if none is available.   This will
-  /// generally be the same as the file descriptor, this function
-  /// is not interchangeable with GetDescriptor().   A WaitableHandle
-  /// must only be used for polling, not actual I/O.
-  ///
-  /// \return
-  ///     a valid handle or IOObject::kInvalidHandleValue
-  WaitableHandle GetWaitableHandle() override;
-
-  /// Get the file specification for this file, if possible.
-  ///
-  /// \param[out] file_spec
-  ///     the file specification.
-  /// \return
-  ///     ENOTSUP, success, or another error.
-  virtual Status GetFileSpec(FileSpec &file_spec) const;
-
-  /// Get underlying OS file descriptor for this file, or kInvalidDescriptor.
-  /// If the descriptor is valid, then it may be used directly for I/O
-  /// However, the File may also perform it's own buffering, so avoid using
-  /// this if it is not necessary, or use Flush() appropriately.
-  ///
-  /// \return
-  ///    a valid file descriptor for this file or kInvalidDescriptor
-  virtual int GetDescriptor() const;
-
-  /// Get the underlying libc stream for this file, or NULL.
-  ///
-  /// Not all valid files will have a FILE* stream.   This should only be
-  /// used if absolutely necessary, such as to interact with 3rd party
-  /// libraries that need FILE* streams.
-  ///
-  /// \return
-  ///    a valid stream or NULL;
-  virtual FILE *GetStream();
-
-  /// Seek to an offset relative to the beginning of the file.
-  ///
-  /// NOTE: This function is NOT thread safe, other threads that
-  /// access this object might also change the current file position. For
-  /// thread safe reads and writes see the following functions: @see
-  /// File::Read (void *, size_t, off_t &) \see File::Write (const void *,
-  /// size_t, off_t &)
-  ///
-  /// \param[in] offset
-  ///     The offset to seek to within the file relative to the
-  ///     beginning of the file.
-  ///
-  /// \param[in] error_ptr
-  ///     A pointer to a lldb_private::Status object that will be
-  ///     filled in if non-nullptr.
-  ///
-  /// \return
-  ///     The resulting seek offset, or -1 on error.
-  virtual off_t SeekFromStart(off_t offset, Status *error_ptr = nullptr);
-
-  /// Seek to an offset relative to the current file position.
-  ///
-  /// NOTE: This function is NOT thread safe, other threads that
-  /// access this object might also change the current file position. For
-  /// thread safe reads and writes see the following functions: @see
-  /// File::Read (void *, size_t, off_t &) \see File::Write (const void *,
-  /// size_t, off_t &)
-  ///
-  /// \param[in] offset
-  ///     The offset to seek to within the file relative to the
-  ///     current file position.
-  ///
-  /// \param[in] error_ptr
-  ///     A pointer to a lldb_private::Status object that will be
-  ///     filled in if non-nullptr.
-  ///
-  /// \return
-  ///     The resulting seek offset, or -1 on error.
-  virtual off_t SeekFromCurrent(off_t offset, Status *error_ptr = nullptr);
-
-  /// Seek to an offset relative to the end of the file.
-  ///
-  /// NOTE: This function is NOT thread safe, other threads that
-  /// access this object might also change the current file position. For
-  /// thread safe reads and writes see the following functions: @see
-  /// File::Read (void *, size_t, off_t &) \see File::Write (const void *,
-  /// size_t, off_t &)
-  ///
-  /// \param[in,out] offset
-  ///     The offset to seek to within the file relative to the
-  ///     end of the file which gets filled in with the resulting
-  ///     absolute file offset.
-  ///
-  /// \param[in] error_ptr
-  ///     A pointer to a lldb_private::Status object that will be
-  ///     filled in if non-nullptr.
-  ///
-  /// \return
-  ///     The resulting seek offset, or -1 on error.
-  virtual off_t SeekFromEnd(off_t offset, Status *error_ptr = nullptr);
-
-  /// Read bytes from a file from the specified file offset.
-  ///
-  /// NOTE: This function is thread safe in that clients manager their
-  /// own file position markers and reads on other threads won't mess up the
-  /// current read.
-  ///
-  /// \param[in] dst
-  ///     A buffer where to put the bytes that are read.
-  ///
-  /// \param[in,out] num_bytes
-  ///     The number of bytes to read from the current file position
-  ///     which gets modified with the number of bytes that were read.
-  ///
-  /// \param[in,out] offset
-  ///     The offset within the file from which to read \a num_bytes
-  ///     bytes. This offset gets incremented by the number of bytes
-  ///     that were read.
-  ///
-  /// \return
-  ///     An error object that indicates success or the reason for
-  ///     failure.
-  virtual Status Read(void *dst, size_t &num_bytes, off_t &offset);
-
-  /// Write bytes to a file at the specified file offset.
-  ///
-  /// NOTE: This function is thread safe in that clients manager their
-  /// own file position markers, though clients will need to implement their
-  /// own locking externally to avoid multiple people writing to the file at
-  /// the same time.
-  ///
-  /// \param[in] src
-  ///     A buffer containing the bytes to write.
-  ///
-  /// \param[in,out] num_bytes
-  ///     The number of bytes to write to the file at offset \a offset.
-  ///     \a num_bytes gets modified with the number of bytes that
-  ///     were read.
-  ///
-  /// \param[in,out] offset
-  ///     The offset within the file at which to write \a num_bytes
-  ///     bytes. This offset gets incremented by the number of bytes
-  ///     that were written.
-  ///
-  /// \return
-  ///     An error object that indicates success or the reason for
-  ///     failure.
-  virtual Status Write(const void *src, size_t &num_bytes, off_t &offset);
-
-  /// Flush the current stream
-  ///
-  /// \return
-  ///     An error object that indicates success or the reason for
-  ///     failure.
-  virtual Status Flush();
-
-  /// Sync to disk.
-  ///
-  /// \return
-  ///     An error object that indicates success or the reason for
-  ///     failure.
-  virtual Status Sync();
-
-  /// Output printf formatted output to the stream.
-  ///
-  /// NOTE: this is not virtual, because it just calls the va_list
-  /// version of the function.
-  ///
-  /// Print some formatted output to the stream.
-  ///
-  /// \param[in] format
-  ///     A printf style format string.
-  ///
-  /// \param[in] ...
-  ///     Variable arguments that are needed for the printf style
-  ///     format string \a format.
-  size_t Printf(const char *format, ...) __attribute__((format(printf, 2, 3)));
-
-  /// Output printf formatted output to the stream.
-  ///
-  /// Print some formatted output to the stream.
-  ///
-  /// \param[in] format
-  ///     A printf style format string.
-  ///
-  /// \param[in] args
-  ///     Variable arguments that are needed for the printf style
-  ///     format string \a format.
-  virtual size_t PrintfVarArg(const char *format, va_list args);
-
-  /// Return the OpenOptions for this file.
-  ///
-  /// Some options like eOpenOptionDontFollowSymlinks only make
-  /// sense when a file is being opened (or not at all)
-  /// and may not be preserved for this method.  But any valid
-  /// File should return either eOpenOptionReadOnly, eOpenOptionWriteOnly
-  /// or eOpenOptionReadWrite here.
-  ///
-  /// \return
-  ///    OpenOptions flags for this file, or an error.
-  virtual llvm::Expected<OpenOptions> GetOptions() const;
-
-  llvm::Expected<const char *> GetOpenMode() const {
-    auto opts = GetOptions();
-    if (!opts)
-      return opts.takeError();
-    return GetStreamOpenModeFromOptions(opts.get());
-  }
-
-  /// Get the permissions for a this file.
-  ///
-  /// \return
-  ///     Bits logical OR'ed together from the permission bits defined
-  ///     in lldb_private::File::Permissions.
-  uint32_t GetPermissions(Status &error) const;
-
-  /// Return true if this file is interactive.
-  ///
-  /// \return
-  ///     True if this file is a terminal (tty or pty), false
-  ///     otherwise.
-  bool GetIsInteractive();
-
-  /// Return true if this file from a real terminal.
-  ///
-  /// Just knowing a file is a interactive isn't enough, we also need to know
-  /// if the terminal has a width and height so we can do cursor movement and
-  /// other terminal manipulations by sending escape sequences.
-  ///
-  /// \return
-  ///     True if this file is a terminal (tty, not a pty) that has
-  ///     a non-zero width and height, false otherwise.
-  bool GetIsRealTerminal();
-
-  /// Return true if this file is a terminal which supports colors.
-  ///
-  /// \return
-  ///    True iff this is a terminal and it supports colors.
-  bool GetIsTerminalWithColors();
-
-  operator bool() const { return IsValid(); };
-
-  bool operator!() const { return !IsValid(); };
 
-  static char ID;
-  virtual bool isA(const void *classID) const { return classID == &ID; }
-  static bool classof(const File *file) { return file->isA(&ID); }
-
-protected:
-  LazyBool m_is_interactive = eLazyBoolCalculate;
-  LazyBool m_is_real_terminal = eLazyBoolCalculate;
-  LazyBool m_supports_colors = eLazyBoolCalculate;
-
-  void CalculateInteractiveAndTerminal();
-
-private:
-  File(const File &) = delete;
-  const File &operator=(const File &) = delete;
-};
-
-class NativeFile : public File {
-public:
-  enum TransferOwnership : bool {
-    Owned = true,
-    Unowned = false,
-  };
+#if defined(_WIN32)
+#include "lldb/Host/windows/FileWindows.h"
+#else
+#include "lldb/Host/posix/FilePosix.h"
+#endif
 
-  NativeFile();
-
-  NativeFile(FILE *fh, OpenOptions options, bool transfer_ownership);
-
-  NativeFile(int fd, OpenOptions options, bool transfer_ownership);
-
-  ~NativeFile() override { Close(); }
-
-  bool IsValid() const override;
-
-  Status Read(void *buf, size_t &num_bytes) override;
-  Status Write(const void *buf, size_t &num_bytes) override;
-  Status Close() override;
-  WaitableHandle GetWaitableHandle() override;
-  Status GetFileSpec(FileSpec &file_spec) const override;
-  int GetDescriptor() const override;
-  FILE *GetStream() override;
-  off_t SeekFromStart(off_t offset, Status *error_ptr = nullptr) override;
-  off_t SeekFromCurrent(off_t offset, Status *error_ptr = nullptr) override;
-  off_t SeekFromEnd(off_t offset, Status *error_ptr = nullptr) override;
-  Status Read(void *dst, size_t &num_bytes, off_t &offset) override;
-  Status Write(const void *src, size_t &num_bytes, off_t &offset) override;
-  Status Flush() override;
-  Status Sync() override;
-  size_t PrintfVarArg(const char *format, va_list args) override;
-  llvm::Expected<OpenOptions> GetOptions() const override;
-
-  static char ID;
-  bool isA(const void *classID) const override {
-    return classID == &ID || File::isA(classID);
-  }
-  static bool classof(const File *file) { return file->isA(&ID); }
-
-protected:
-  struct ValueGuard {
-    ValueGuard(std::mutex &m, bool b) : guard(m, std::adopt_lock), value(b) {}
-    std::lock_guard<std::mutex> guard;
-    bool value;
-    operator bool() { return value; }
-  };
-
-  bool DescriptorIsValidUnlocked() const {
-
-    return File::DescriptorIsValid(m_descriptor);
-  }
-
-  bool StreamIsValidUnlocked() const { return m_stream != kInvalidStream; }
-
-  ValueGuard DescriptorIsValid() const {
-    m_descriptor_mutex.lock();
-    return ValueGuard(m_descriptor_mutex, DescriptorIsValidUnlocked());
-  }
-
-  ValueGuard StreamIsValid() const {
-    m_stream_mutex.lock();
-    return ValueGuard(m_stream_mutex, StreamIsValidUnlocked());
-  }
-
-  int m_descriptor = kInvalidDescriptor;
-  bool m_own_descriptor = false;
-  mutable std::mutex m_descriptor_mutex;
-
-  FILE *m_stream = kInvalidStream;
-  mutable std::mutex m_stream_mutex;
-
-  OpenOptions m_options{};
-  bool m_own_stream = false;
-  std::mutex offset_access_mutex;
-
-  bool is_windows_console = false;
+namespace lldb_private {
 
-private:
-  NativeFile(const NativeFile &) = delete;
-  const NativeFile &operator=(const NativeFile &) = delete;
-};
+#if defined(_WIN32)
+typedef NativeFileWindows NativeFile;
+#else
+typedef NativeFilePosix NativeFile;
+#endif
 
 class SerialPort : public NativeFile {
 public:
diff --git a/lldb/include/lldb/Host/FileBase.h 
b/lldb/include/lldb/Host/FileBase.h
new file mode 100644
index 0000000000000..51a0de9a93887
--- /dev/null
+++ b/lldb/include/lldb/Host/FileBase.h
@@ -0,0 +1,245 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_HOST_FILEBASE_H
+#define LLDB_HOST_FILEBASE_H
+
+#include "lldb/Host/PosixApi.h"
+#include "lldb/Utility/IOObject.h"
+#include "lldb/Utility/Status.h"
+#include "lldb/lldb-private.h"
+#include "llvm/ADT/BitmaskEnum.h"
+
+#include <cstdarg>
+#include <cstdio>
+#include <mutex>
+#include <sys/types.h>
+
+namespace lldb_private {
+
+LLVM_ENABLE_BITMASK_ENUMS_IN_NAMESPACE();
+
+/// \class File File.h "lldb/Host/File.h"
+/// An abstract base class for files.
+///
+/// Files will often be NativeFiles, which provides a wrapper
+/// around host OS file functionality.   But it
+/// is also possible to subclass file to provide objects that have file
+/// or stream functionality but are not backed by any host OS file.
+class File : public IOObject {
+public:
+  static int kInvalidDescriptor;
+  static FILE *kInvalidStream;
+
+  // NB this enum is used in the lldb platform gdb-remote packet
+  // vFile:open: and existing values cannot be modified.
+  //
+  // The first set of values is defined by gdb headers and can be found
+  // in the documentation at:
+  // * https://sourceware.org/gdb/onlinedocs/gdb/Open-Flags.html#Open-Flags
+  //
+  // The second half are LLDB extensions and use the highest uint32_t bits
+  // to avoid risk of collisions with future gdb remote protocol changes.
+  enum OpenOptions : uint32_t {
+    eOpenOptionReadOnly = 0x0,  // Open file for reading (only)
+    eOpenOptionWriteOnly = 0x1, // Open file for writing (only)
+    eOpenOptionReadWrite = 0x2, // Open file for both reading and writing
+    eOpenOptionAppend =
+        0x8, // Don't truncate file when opening, append to end of file
+    eOpenOptionCanCreate = 0x200, // Create file if doesn't already exist
+    eOpenOptionTruncate = 0x400,  // Truncate file when opening
+    eOpenOptionCanCreateNewOnly =
+        0x800, // Can create file only if it doesn't already exist
+
+    eOpenOptionNonBlocking = (1u << 28), // File reads
+    eOpenOptionDontFollowSymlinks = (1u << 29),
+    eOpenOptionCloseOnExec =
+        (1u << 30), // Close the file when executing a new process
+    eOpenOptionInvalid = (1u << 31), // Used as invalid value
+    LLVM_MARK_AS_BITMASK_ENUM(/* largest_value= */ eOpenOptionInvalid)
+  };
+
+  static constexpr OpenOptions OpenOptionsModeMask =
+      eOpenOptionReadOnly | eOpenOptionWriteOnly | eOpenOptionReadWrite;
+
+  static mode_t ConvertOpenOptionsForPOSIXOpen(OpenOptions open_options);
+  static llvm::Expected<OpenOptions> GetOptionsFromMode(llvm::StringRef mode);
+  static bool DescriptorIsValid(int descriptor) { return descriptor >= 0; };
+  static llvm::Expected<const char *>
+  GetStreamOpenModeFromOptions(OpenOptions options);
+
+  File() : IOObject(eFDTypeFile) {};
+
+  Status Read(void *buf, size_t &num_bytes) override;
+  Status Write(const void *buf, size_t &num_bytes) override;
+  bool IsValid() const override;
+  Status Close() override;
+  WaitableHandle GetWaitableHandle() override;
+
+  virtual Status GetFileSpec(FileSpec &file_spec) const;
+  virtual int GetDescriptor() const;
+  virtual FILE *GetStream();
+  virtual off_t SeekFromStart(off_t offset, Status *error_ptr = nullptr);
+  virtual off_t SeekFromCurrent(off_t offset, Status *error_ptr = nullptr);
+  virtual off_t SeekFromEnd(off_t offset, Status *error_ptr = nullptr);
+  virtual Status Read(void *dst, size_t &num_bytes, off_t &offset);
+  virtual Status Write(const void *src, size_t &num_bytes, off_t &offset);
+  virtual Status Flush();
+  virtual Status Sync();
+
+  size_t Printf(const char *format, ...) __attribute__((format(printf, 2, 3)));
+  virtual size_t PrintfVarArg(const char *format, va_list args);
+
+  virtual llvm::Expected<OpenOptions> GetOptions() const;
+
+  llvm::Expected<const char *> GetOpenMode() const {
+    auto opts = GetOptions();
+    if (!opts)
+      return opts.takeError();
+    return GetStreamOpenModeFromOptions(opts.get());
+  }
+
+  uint32_t GetPermissions(Status &error) const;
+
+  bool GetIsInteractive();
+  bool GetIsRealTerminal();
+  bool GetIsTerminalWithColors();
+
+  operator bool() const { return IsValid(); };
+  bool operator!() const { return !IsValid(); };
+
+  static char ID;
+  virtual bool isA(const void *classID) const { return classID == &ID; }
+  static bool classof(const File *file) { return file->isA(&ID); }
+
+protected:
+  LazyBool m_is_interactive = eLazyBoolCalculate;
+  LazyBool m_is_real_terminal = eLazyBoolCalculate;
+  LazyBool m_supports_colors = eLazyBoolCalculate;
+
+  /// Refresh the cached interactive / terminal / color flags by inspecting
+  /// the underlying descriptor.  The default implementation leaves the
+  /// flags set to "no"; concrete subclasses override this with the
+  /// appropriate platform-specific terminal probing.
+  virtual void CalculateInteractiveAndTerminal();
+
+private:
+  File(const File &) = delete;
+  const File &operator=(const File &) = delete;
+};
+
+/// \class NativeFileBase File.h "lldb/Host/File.h"
+/// Common base for the platform-specific NativeFile implementations.
+///
+/// Holds the descriptor / stream state and provides the platform-neutral
+/// method bodies.  The platform-specific behaviour lives in
+/// NativeFilePosix (Host/posix/FilePosix.h) and NativeFileWindows
+/// (Host/windows/FileWindows.h); user code should refer to the
+/// `NativeFile` typedef in lldb/Host/File.h, which resolves to the right
+/// concrete class for the host.
+class NativeFileBase : public File {
+public:
+  enum TransferOwnership : bool {
+    Owned = true,
+    Unowned = false,
+  };
+
+  NativeFileBase();
+
+  NativeFileBase(FILE *fh, OpenOptions options, bool transfer_ownership);
+
+  NativeFileBase(int fd, OpenOptions options, bool transfer_ownership);
+
+  ~NativeFileBase() override { Close(); }
+
+  bool IsValid() const override;
+
+  Status Read(void *buf, size_t &num_bytes) override;
+  Status Write(const void *buf, size_t &num_bytes) override;
+  Status Close() override;
+  WaitableHandle GetWaitableHandle() override;
+  Status GetFileSpec(FileSpec &file_spec) const override;
+  int GetDescriptor() const override;
+  FILE *GetStream() override;
+  off_t SeekFromStart(off_t offset, Status *error_ptr = nullptr) override;
+  off_t SeekFromCurrent(off_t offset, Status *error_ptr = nullptr) override;
+  off_t SeekFromEnd(off_t offset, Status *error_ptr = nullptr) override;
+  Status Read(void *dst, size_t &num_bytes, off_t &offset) override;
+  Status Write(const void *src, size_t &num_bytes, off_t &offset) override;
+  Status Flush() override;
+  Status Sync() override;
+  size_t PrintfVarArg(const char *format, va_list args) override;
+  llvm::Expected<OpenOptions> GetOptions() const override;
+
+  static char ID;
+  bool isA(const void *classID) const override {
+    return classID == &ID || File::isA(classID);
+  }
+  static bool classof(const File *file) { return file->isA(&ID); }
+
+protected:
+  struct ValueGuard {
+    ValueGuard(std::mutex &m, bool b) : guard(m, std::adopt_lock), value(b) {}
+    std::lock_guard<std::mutex> guard;
+    bool value;
+    operator bool() { return value; }
+  };
+
+  bool DescriptorIsValidUnlocked() const {
+    return File::DescriptorIsValid(m_descriptor);
+  }
+
+  bool StreamIsValidUnlocked() const { return m_stream != kInvalidStream; }
+
+  ValueGuard DescriptorIsValid() const {
+    m_descriptor_mutex.lock();
+    return ValueGuard(m_descriptor_mutex, DescriptorIsValidUnlocked());
+  }
+
+  ValueGuard StreamIsValid() const {
+    m_stream_mutex.lock();
+    return ValueGuard(m_stream_mutex, StreamIsValidUnlocked());
+  }
+
+  /// Map a stream to its underlying file descriptor.  Subclasses override
+  /// when the host libc spells fileno() differently (e.g. _fileno on
+  /// Windows MSVC).
+  virtual int Fileno(FILE *fh) const;
+
+  /// Duplicate a file descriptor.  Subclasses override when the host libc
+  /// spells dup() differently (e.g. _dup on Windows MSVC).
+  virtual int Dup(int fd) const;
+
+  /// Hook for stream writes that bypass the default fwrite path.  Returns
+  /// true if the bytes were consumed by the override (e.g. the Windows
+  /// console output path); false to fall through to the default fwrite.
+  /// Called with m_stream_mutex held.
+  virtual bool TryWriteStreamUnlocked(const void *buf, size_t &num_bytes,
+                                      Status &error) {
+    return false;
+  }
+
+  int m_descriptor = kInvalidDescriptor;
+  bool m_own_descriptor = false;
+  mutable std::mutex m_descriptor_mutex;
+
+  FILE *m_stream = kInvalidStream;
+  mutable std::mutex m_stream_mutex;
+
+  OpenOptions m_options{};
+  bool m_own_stream = false;
+  std::mutex offset_access_mutex;
+
+private:
+  NativeFileBase(const NativeFileBase &) = delete;
+  const NativeFileBase &operator=(const NativeFileBase &) = delete;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_HOST_FILEBASE_H
diff --git a/lldb/include/lldb/Host/posix/FilePosix.h 
b/lldb/include/lldb/Host/posix/FilePosix.h
new file mode 100644
index 0000000000000..e55d9a0338897
--- /dev/null
+++ b/lldb/include/lldb/Host/posix/FilePosix.h
@@ -0,0 +1,49 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_HOST_POSIX_FILEPOSIX_H
+#define LLDB_HOST_POSIX_FILEPOSIX_H
+
+#include "lldb/Host/FileBase.h"
+
+namespace lldb_private {
+
+/// \class NativeFilePosix FilePosix.h "lldb/Host/posix/FilePosix.h"
+/// POSIX implementation of NativeFile.
+///
+/// Most of the work happens in NativeFileBase; this class adds POSIX-flavoured
+/// terminal probing and the debug-mode access-mode assertion that fires when a
+/// FILE* is wrapped with mismatched OpenOptions.
+class NativeFilePosix : public NativeFileBase {
+public:
+  NativeFilePosix() = default;
+
+  NativeFilePosix(FILE *fh, OpenOptions options, bool transfer_ownership);
+
+  NativeFilePosix(int fd, OpenOptions options, bool transfer_ownership);
+
+  // Bring the inherited single-argument Read/Write into scope so they aren't
+  // hidden by the positional overloads declared below.
+  using NativeFileBase::Read;
+  using NativeFileBase::Write;
+
+  Status GetFileSpec(FileSpec &file_spec) const override;
+  Status Read(void *dst, size_t &num_bytes, off_t &offset) override;
+  Status Write(const void *src, size_t &num_bytes, off_t &offset) override;
+
+protected:
+  void CalculateInteractiveAndTerminal() override;
+
+private:
+  NativeFilePosix(const NativeFilePosix &) = delete;
+  const NativeFilePosix &operator=(const NativeFilePosix &) = delete;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_HOST_POSIX_FILEPOSIX_H
diff --git a/lldb/include/lldb/Host/windows/FileWindows.h 
b/lldb/include/lldb/Host/windows/FileWindows.h
new file mode 100644
index 0000000000000..a6c2c6bb03b21
--- /dev/null
+++ b/lldb/include/lldb/Host/windows/FileWindows.h
@@ -0,0 +1,65 @@
+//===----------------------------------------------------------------------===//
+//
+// 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_HOST_WINDOWS_FILEWINDOWS_H
+#define LLDB_HOST_WINDOWS_FILEWINDOWS_H
+
+#include "lldb/Host/FileBase.h"
+
+namespace lldb_private {
+
+/// \class NativeFileWindows FileWindows.h "lldb/Host/windows/FileWindows.h"
+/// Windows implementation of NativeFile.
+///
+/// Overrides the bits of NativeFileBase that need to call Win32 directly:
+/// terminal probing, FlushFileBuffers-based Sync, the OS-handle waitable,
+/// the lseek/SeekFromStart positional read/write fallback (Windows lacks
+/// pread/pwrite), and the console output path that bypasses fwrite for
+/// proper non-ASCII rendering.
+class NativeFileWindows : public NativeFileBase {
+public:
+  NativeFileWindows() = default;
+
+  NativeFileWindows(FILE *fh, OpenOptions options, bool transfer_ownership);
+
+  NativeFileWindows(int fd, OpenOptions options, bool transfer_ownership);
+
+  // Bring the inherited single-argument Read/Write into scope so they aren't
+  // hidden by the positional overloads declared below.
+  using NativeFileBase::Read;
+  using NativeFileBase::Write;
+
+  WaitableHandle GetWaitableHandle() override;
+  Status Sync() override;
+  Status Read(void *dst, size_t &num_bytes, off_t &offset) override;
+  Status Write(const void *src, size_t &num_bytes, off_t &offset) override;
+
+protected:
+  void CalculateInteractiveAndTerminal() override;
+
+  int Fileno(FILE *fh) const override;
+  int Dup(int fd) const override;
+
+  bool TryWriteStreamUnlocked(const void *buf, size_t &num_bytes,
+                              Status &error) override;
+
+private:
+  /// Set when this file wraps stdin/stdout/stderr connected to a console;
+  /// triggers the raw_fd_ostream path for correct non-ASCII output.
+  bool m_is_windows_console = false;
+
+  void DetectIsWindowsConsole(FILE *fh);
+  void DetectIsWindowsConsole(int fd);
+
+  NativeFileWindows(const NativeFileWindows &) = delete;
+  const NativeFileWindows &operator=(const NativeFileWindows &) = delete;
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_HOST_WINDOWS_FILEWINDOWS_H
diff --git a/lldb/source/Host/CMakeLists.txt b/lldb/source/Host/CMakeLists.txt
index e1329af67de32..e1db2ed35485b 100644
--- a/lldb/source/Host/CMakeLists.txt
+++ b/lldb/source/Host/CMakeLists.txt
@@ -71,6 +71,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Windows")
     windows/ConnectionConPTYWindows.cpp
     windows/ConnectionGenericFileWindows.cpp
     windows/FileSystem.cpp
+    windows/FileWindows.cpp
     windows/Host.cpp
     windows/HostInfoWindows.cpp
     windows/HostProcessWindows.cpp
@@ -86,6 +87,7 @@ if (CMAKE_SYSTEM_NAME MATCHES "Windows")
 else()
   add_host_subdirectory(posix
     posix/DomainSocket.cpp
+    posix/FilePosix.cpp
     posix/FileSystemPosix.cpp
     posix/HostInfoPosix.cpp
     posix/HostProcessPosix.cpp
diff --git a/lldb/source/Host/common/File.cpp b/lldb/source/Host/common/File.cpp
index 9ffdf014a8bdb..0a22c8f949547 100644
--- a/lldb/source/Host/common/File.cpp
+++ b/lldb/source/Host/common/File.cpp
@@ -1,4 +1,4 @@
-//===-- File.cpp 
----------------------------------------------------------===//
+//===----------------------------------------------------------------------===//
 //
 // Part of the LLVM Project, under the Apache License v2.0 with LLVM 
Exceptions.
 // See https://llvm.org/LICENSE.txt for license information.
@@ -14,15 +14,7 @@
 #include <cstdio>
 #include <fcntl.h>
 #include <optional>
-
-#ifdef _WIN32
-#include "lldb/Host/windows/windows.h"
-#else
-#include <sys/ioctl.h>
 #include <sys/stat.h>
-#include <termios.h>
-#include <unistd.h>
-#endif
 
 #include "lldb/Host/Config.h"
 #include "lldb/Host/FileSystem.h"
@@ -158,36 +150,12 @@ Status File::Flush() { return Status(); }
 Status File::Sync() { return Flush(); }
 
 void File::CalculateInteractiveAndTerminal() {
-  const int fd = GetDescriptor();
-  if (!DescriptorIsValid(fd)) {
-    m_is_interactive = eLazyBoolNo;
-    m_is_real_terminal = eLazyBoolNo;
-    m_supports_colors = eLazyBoolNo;
-    return;
-  }
+  // The base class has no descriptor of its own; concrete subclasses probe
+  // the platform.  Default to "not a terminal" so callers see consistent
+  // values when they didn't override.
   m_is_interactive = eLazyBoolNo;
   m_is_real_terminal = eLazyBoolNo;
-#if defined(_WIN32)
-  if (_isatty(fd)) {
-    m_is_interactive = eLazyBoolYes;
-    m_is_real_terminal = eLazyBoolYes;
-#if defined(ENABLE_VIRTUAL_TERMINAL_PROCESSING)
-    m_supports_colors = eLazyBoolYes;
-#endif
-  }
-#else
-  if (isatty(fd)) {
-    m_is_interactive = eLazyBoolYes;
-    struct winsize window_size;
-    if (::ioctl(fd, TIOCGWINSZ, &window_size) == 0) {
-      if (window_size.ws_col > 0) {
-        m_is_real_terminal = eLazyBoolYes;
-        if (llvm::sys::Process::FileDescriptorHasColors(fd))
-          m_supports_colors = eLazyBoolYes;
-      }
-    }
-  }
-#endif
+  m_supports_colors = eLazyBoolNo;
 }
 
 bool File::GetIsInteractive() {
@@ -247,78 +215,32 @@ uint32_t File::GetPermissions(Status &error) const {
   return file_stats.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO);
 }
 
-NativeFile::NativeFile() = default;
+NativeFileBase::NativeFileBase() = default;
 
-NativeFile::NativeFile(FILE *fh, OpenOptions options, bool transfer_ownership)
-    : m_stream(fh), m_options(options), m_own_stream(transfer_ownership) {
-#ifdef _WIN32
-  // In order to properly display non ASCII characters in Windows, we need to
-  // use Windows APIs to print to the console. This is only required if the
-  // stream outputs to a console.
-  {
-    HANDLE h = INVALID_HANDLE_VALUE;
-    if (fh == stdin)
-      h = ::GetStdHandle(STD_INPUT_HANDLE);
-    else if (fh == stdout)
-      h = ::GetStdHandle(STD_OUTPUT_HANDLE);
-    else if (fh == stderr)
-      h = ::GetStdHandle(STD_ERROR_HANDLE);
-    is_windows_console =
-        h != INVALID_HANDLE_VALUE && ::GetFileType(h) == FILE_TYPE_CHAR;
-  }
-#else
-#ifndef NDEBUG
-  int fd = fileno(fh);
-  if (fd != -1) {
-    int required_mode = ConvertOpenOptionsForPOSIXOpen(options) & O_ACCMODE;
-    int mode = fcntl(fd, F_GETFL);
-    if (mode != -1) {
-      mode &= O_ACCMODE;
-      // Check that the file is open with a valid subset of the requested file
-      // access mode, e.g. if we expected the file to be writable then ensure 
it
-      // was opened with O_WRONLY or O_RDWR.
-      assert(
-          (required_mode == O_RDWR && mode == O_RDWR) ||
-          (required_mode == O_RDONLY && (mode == O_RDWR || mode == O_RDONLY) ||
-           (required_mode == O_WRONLY &&
-            (mode == O_RDWR || mode == O_WRONLY))) &&
-              "invalid file access mode");
-    }
-  }
-#endif
-#endif
-}
+NativeFileBase::NativeFileBase(FILE *fh, OpenOptions options,
+                               bool transfer_ownership)
+    : m_stream(fh), m_options(options), m_own_stream(transfer_ownership) {}
 
-NativeFile::NativeFile(int fd, OpenOptions options, bool transfer_ownership)
+NativeFileBase::NativeFileBase(int fd, OpenOptions options,
+                               bool transfer_ownership)
     : m_descriptor(fd), m_own_descriptor(transfer_ownership),
-      m_options(options) {
-#ifdef _WIN32
-  // In order to properly display non ASCII characters in Windows, we need to
-  // use Windows APIs to print to the console. This is only required if the
-  // file outputs to a console.
-  {
-    HANDLE h = INVALID_HANDLE_VALUE;
-    if (fd == STDIN_FILENO)
-      h = ::GetStdHandle(STD_INPUT_HANDLE);
-    else if (fd == STDOUT_FILENO)
-      h = ::GetStdHandle(STD_OUTPUT_HANDLE);
-    else if (fd == STDERR_FILENO)
-      h = ::GetStdHandle(STD_ERROR_HANDLE);
-    is_windows_console =
-        h != INVALID_HANDLE_VALUE && ::GetFileType(h) == FILE_TYPE_CHAR;
-  }
-#endif
-}
+      m_options(options) {}
 
-bool NativeFile::IsValid() const {
+bool NativeFileBase::IsValid() const {
   std::scoped_lock<std::mutex, std::mutex> lock(m_descriptor_mutex,
                                                 m_stream_mutex);
   return DescriptorIsValidUnlocked() || StreamIsValidUnlocked();
 }
 
-Expected<File::OpenOptions> NativeFile::GetOptions() const { return m_options; 
}
+Expected<File::OpenOptions> NativeFileBase::GetOptions() const {
+  return m_options;
+}
+
+int NativeFileBase::Fileno(FILE *fh) const { return ::fileno(fh); }
 
-int NativeFile::GetDescriptor() const {
+int NativeFileBase::Dup(int fd) const { return ::dup(fd); }
+
+int NativeFileBase::GetDescriptor() const {
   if (ValueGuard descriptor_guard = DescriptorIsValid()) {
     return m_descriptor;
   }
@@ -326,26 +248,18 @@ int NativeFile::GetDescriptor() const {
   // Don't open the file descriptor if we don't need to, just get it from the
   // stream if we have one.
   if (ValueGuard stream_guard = StreamIsValid()) {
-#if defined(_WIN32)
-    return _fileno(m_stream);
-#else
-    return fileno(m_stream);
-#endif
+    return Fileno(m_stream);
   }
 
   // Invalid descriptor and invalid stream, return invalid descriptor.
   return kInvalidDescriptor;
 }
 
-IOObject::WaitableHandle NativeFile::GetWaitableHandle() {
-#ifdef _WIN32
-  return (HANDLE)_get_osfhandle(GetDescriptor());
-#else
+IOObject::WaitableHandle NativeFileBase::GetWaitableHandle() {
   return GetDescriptor();
-#endif
 }
 
-FILE *NativeFile::GetStream() {
+FILE *NativeFileBase::GetStream() {
   ValueGuard stream_guard = StreamIsValid();
   if (!stream_guard) {
     if (ValueGuard descriptor_guard = DescriptorIsValid()) {
@@ -354,13 +268,9 @@ FILE *NativeFile::GetStream() {
         llvm::consumeError(mode.takeError());
       else {
         if (!m_own_descriptor) {
-// We must duplicate the file descriptor if we don't own it because when you
-// call fdopen, the stream will own the fd
-#ifdef _WIN32
-          m_descriptor = ::_dup(m_descriptor);
-#else
-          m_descriptor = dup(m_descriptor);
-#endif
+          // We must duplicate the file descriptor if we don't own it because
+          // when you call fdopen, the stream will own the fd.
+          m_descriptor = Dup(m_descriptor);
           m_own_descriptor = true;
         }
 
@@ -380,7 +290,7 @@ FILE *NativeFile::GetStream() {
   return m_stream;
 }
 
-Status NativeFile::Close() {
+Status NativeFileBase::Close() {
   std::scoped_lock<std::mutex, std::mutex> lock(m_descriptor_mutex,
                                                 m_stream_mutex);
 
@@ -417,43 +327,16 @@ Status NativeFile::Close() {
   return error;
 }
 
-Status NativeFile::GetFileSpec(FileSpec &file_spec) const {
-  Status error;
-#ifdef F_GETPATH
-  if (IsValid()) {
-    char path[PATH_MAX];
-    if (::fcntl(GetDescriptor(), F_GETPATH, path) == -1)
-      error = Status::FromErrno();
-    else
-      file_spec.SetFile(path, FileSpec::Style::native);
-  } else {
-    error = Status::FromErrorString("invalid file handle");
-  }
-#elif defined(__linux__)
-  char proc[64];
-  char path[PATH_MAX];
-  if (::snprintf(proc, sizeof(proc), "/proc/self/fd/%d", GetDescriptor()) < 0)
-    error = Status::FromErrorString("cannot resolve file descriptor");
-  else {
-    ssize_t len;
-    if ((len = ::readlink(proc, path, sizeof(path) - 1)) == -1)
-      error = Status::FromErrno();
-    else {
-      path[len] = '\0';
-      file_spec.SetFile(path, FileSpec::Style::native);
-    }
-  }
-#else
-  error = Status::FromErrorString(
+Status NativeFileBase::GetFileSpec(FileSpec &file_spec) const {
+  // Default: not supported.  POSIX subclass overrides with F_GETPATH /
+  // /proc/self/fd lookups; the Windows subclass currently has no equivalent
+  // and inherits this behaviour.
+  file_spec.Clear();
+  return Status::FromErrorString(
       "NativeFile::GetFileSpec is not supported on this platform");
-#endif
-
-  if (error.Fail())
-    file_spec.Clear();
-  return error;
 }
 
-off_t NativeFile::SeekFromStart(off_t offset, Status *error_ptr) {
+off_t NativeFileBase::SeekFromStart(off_t offset, Status *error_ptr) {
   off_t result = 0;
   if (ValueGuard descriptor_guard = DescriptorIsValid()) {
     result = ::lseek(m_descriptor, offset, SEEK_SET);
@@ -484,7 +367,7 @@ off_t NativeFile::SeekFromStart(off_t offset, Status 
*error_ptr) {
   return result;
 }
 
-off_t NativeFile::SeekFromCurrent(off_t offset, Status *error_ptr) {
+off_t NativeFileBase::SeekFromCurrent(off_t offset, Status *error_ptr) {
   off_t result = -1;
   if (ValueGuard descriptor_guard = DescriptorIsValid()) {
     result = ::lseek(m_descriptor, offset, SEEK_CUR);
@@ -515,7 +398,7 @@ off_t NativeFile::SeekFromCurrent(off_t offset, Status 
*error_ptr) {
   return result;
 }
 
-off_t NativeFile::SeekFromEnd(off_t offset, Status *error_ptr) {
+off_t NativeFileBase::SeekFromEnd(off_t offset, Status *error_ptr) {
   off_t result = -1;
   if (ValueGuard descriptor_guard = DescriptorIsValid()) {
     result = ::lseek(m_descriptor, offset, SEEK_END);
@@ -546,7 +429,7 @@ off_t NativeFile::SeekFromEnd(off_t offset, Status 
*error_ptr) {
   return result;
 }
 
-Status NativeFile::Flush() {
+Status NativeFileBase::Flush() {
   Status error;
   if (ValueGuard stream_guard = StreamIsValid()) {
     if (llvm::sys::RetryAfterSignal(EOF, ::fflush, m_stream) == EOF)
@@ -562,17 +445,11 @@ Status NativeFile::Flush() {
   return error;
 }
 
-Status NativeFile::Sync() {
+Status NativeFileBase::Sync() {
   Status error;
   if (ValueGuard descriptor_guard = DescriptorIsValid()) {
-#ifdef _WIN32
-    int err = FlushFileBuffers((HANDLE)_get_osfhandle(m_descriptor));
-    if (err == 0)
-      error = Status::FromErrorString("unknown error");
-#else
     if (llvm::sys::RetryAfterSignal(-1, ::fsync, m_descriptor) == -1)
       error = Status::FromErrno();
-#endif
   } else {
     error = Status::FromErrorString("invalid file handle");
   }
@@ -585,7 +462,7 @@ Status NativeFile::Sync() {
 #define MAX_WRITE_SIZE INT_MAX
 #endif
 
-Status NativeFile::Read(void *buf, size_t &num_bytes) {
+Status NativeFileBase::Read(void *buf, size_t &num_bytes) {
   Status error;
 
   // Ensure the file is open for reading.
@@ -653,7 +530,7 @@ Status NativeFile::Read(void *buf, size_t &num_bytes) {
   return error;
 }
 
-Status NativeFile::Write(const void *buf, size_t &num_bytes) {
+Status NativeFileBase::Write(const void *buf, size_t &num_bytes) {
   Status error;
 
   // Ensure the file is open for writing.
@@ -703,13 +580,8 @@ Status NativeFile::Write(const void *buf, size_t 
&num_bytes) {
   }
 
   if (ValueGuard stream_guard = StreamIsValid()) {
-#ifdef _WIN32
-    if (is_windows_console) {
-      llvm::raw_fd_ostream(_fileno(m_stream), false)
-          .write((const char *)buf, num_bytes);
+    if (TryWriteStreamUnlocked(buf, num_bytes, error))
       return error;
-    }
-#endif
     bytes_written = ::fwrite(buf, 1, num_bytes, m_stream);
 
     if (bytes_written == 0) {
@@ -728,131 +600,23 @@ Status NativeFile::Write(const void *buf, size_t 
&num_bytes) {
   return error;
 }
 
-Status NativeFile::Read(void *buf, size_t &num_bytes, off_t &offset) {
-  Status error;
-
-#if defined(MAX_READ_SIZE)
-  if (num_bytes > MAX_READ_SIZE) {
-    uint8_t *p = (uint8_t *)buf;
-    size_t bytes_left = num_bytes;
-    // Init the num_bytes read to zero
-    num_bytes = 0;
-
-    while (bytes_left > 0) {
-      size_t curr_num_bytes;
-      if (bytes_left > MAX_READ_SIZE)
-        curr_num_bytes = MAX_READ_SIZE;
-      else
-        curr_num_bytes = bytes_left;
-
-      error = Read(p + num_bytes, curr_num_bytes, offset);
-
-      // Update how many bytes were read
-      num_bytes += curr_num_bytes;
-      if (bytes_left < curr_num_bytes)
-        bytes_left = 0;
-      else
-        bytes_left -= curr_num_bytes;
-
-      if (error.Fail())
-        break;
-    }
-    return error;
-  }
-#endif
-
-#ifndef _WIN32
-  int fd = GetDescriptor();
-  if (fd != kInvalidDescriptor) {
-    ssize_t bytes_read =
-        llvm::sys::RetryAfterSignal(-1, ::pread, fd, buf, num_bytes, offset);
-    if (bytes_read < 0) {
-      num_bytes = 0;
-      error = Status::FromErrno();
-    } else {
-      offset += bytes_read;
-      num_bytes = bytes_read;
-    }
-  } else {
-    num_bytes = 0;
-    error = Status::FromErrorString("invalid file handle");
-  }
-#else
-  std::lock_guard<std::mutex> guard(offset_access_mutex);
-  long cur = ::lseek(m_descriptor, 0, SEEK_CUR);
-  SeekFromStart(offset);
-  error = Read(buf, num_bytes);
-  if (!error.Fail())
-    SeekFromStart(cur);
-#endif
-  return error;
+Status NativeFileBase::Read(void *buf, size_t &num_bytes, off_t &offset) {
+  // Default: not supported.  POSIX subclass implements this with pread();
+  // Windows subclass emulates it with lseek/Read since Win32 lacks pread.
+  num_bytes = 0;
+  return Status::FromErrorString(
+      "positional Read is not supported on this platform");
 }
 
-Status NativeFile::Write(const void *buf, size_t &num_bytes, off_t &offset) {
-  Status error;
-
-#if defined(MAX_WRITE_SIZE)
-  if (num_bytes > MAX_WRITE_SIZE) {
-    const uint8_t *p = (const uint8_t *)buf;
-    size_t bytes_left = num_bytes;
-    // Init the num_bytes written to zero
-    num_bytes = 0;
-
-    while (bytes_left > 0) {
-      size_t curr_num_bytes;
-      if (bytes_left > MAX_WRITE_SIZE)
-        curr_num_bytes = MAX_WRITE_SIZE;
-      else
-        curr_num_bytes = bytes_left;
-
-      error = Write(p + num_bytes, curr_num_bytes, offset);
-
-      // Update how many bytes were read
-      num_bytes += curr_num_bytes;
-      if (bytes_left < curr_num_bytes)
-        bytes_left = 0;
-      else
-        bytes_left -= curr_num_bytes;
-
-      if (error.Fail())
-        break;
-    }
-    return error;
-  }
-#endif
-
-  int fd = GetDescriptor();
-  if (fd != kInvalidDescriptor) {
-#ifndef _WIN32
-    ssize_t bytes_written = llvm::sys::RetryAfterSignal(
-        -1, ::pwrite, m_descriptor, buf, num_bytes, offset);
-    if (bytes_written < 0) {
-      num_bytes = 0;
-      error = Status::FromErrno();
-    } else {
-      offset += bytes_written;
-      num_bytes = bytes_written;
-    }
-#else
-    std::lock_guard<std::mutex> guard(offset_access_mutex);
-    long cur = ::lseek(m_descriptor, 0, SEEK_CUR);
-    SeekFromStart(offset);
-    error = Write(buf, num_bytes);
-    long after = ::lseek(m_descriptor, 0, SEEK_CUR);
-
-    if (!error.Fail())
-      SeekFromStart(cur);
-
-    offset = after;
-#endif
-  } else {
-    num_bytes = 0;
-    error = Status::FromErrorString("invalid file handle");
-  }
-  return error;
+Status NativeFileBase::Write(const void *buf, size_t &num_bytes,
+                             off_t &offset) {
+  // Default: not supported.  See Read(off_t) above.
+  num_bytes = 0;
+  return Status::FromErrorString(
+      "positional Write is not supported on this platform");
 }
 
-size_t NativeFile::PrintfVarArg(const char *format, va_list args) {
+size_t NativeFileBase::PrintfVarArg(const char *format, va_list args) {
   if (StreamIsValid()) {
     return ::vfprintf(m_stream, format, args);
   } else {
@@ -988,5 +752,5 @@ Status SerialPort::Close() {
 }
 
 char File::ID = 0;
-char NativeFile::ID = 0;
+char NativeFileBase::ID = 0;
 char SerialPort::ID = 0;
diff --git a/lldb/source/Host/posix/FilePosix.cpp 
b/lldb/source/Host/posix/FilePosix.cpp
new file mode 100644
index 0000000000000..fb18420435eae
--- /dev/null
+++ b/lldb/source/Host/posix/FilePosix.cpp
@@ -0,0 +1,220 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "lldb/Host/posix/FilePosix.h"
+
+#include <cassert>
+#include <climits>
+#include <fcntl.h>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+#include "lldb/Utility/FileSpec.h"
+#include "lldb/Utility/Status.h"
+#include "llvm/Support/Errno.h"
+#include "llvm/Support/Process.h"
+
+using namespace lldb_private;
+
+#if defined(__APPLE__)
+// Darwin kernels only can read/write <= INT_MAX bytes.  Match the chunking
+// limit used by NativeFileBase::Read/Write.
+#define MAX_READ_SIZE INT_MAX
+#define MAX_WRITE_SIZE INT_MAX
+#endif
+
+NativeFilePosix::NativeFilePosix(FILE *fh, OpenOptions options,
+                                 bool transfer_ownership)
+    : NativeFileBase(fh, options, transfer_ownership) {
+#ifndef NDEBUG
+  int fd = fileno(fh);
+  if (fd != -1) {
+    int required_mode = ConvertOpenOptionsForPOSIXOpen(options) & O_ACCMODE;
+    int mode = fcntl(fd, F_GETFL);
+    if (mode != -1) {
+      mode &= O_ACCMODE;
+      // Check that the file is open with a valid subset of the requested file
+      // access mode, e.g. if we expected the file to be writable then ensure 
it
+      // was opened with O_WRONLY or O_RDWR.
+      assert(
+          (required_mode == O_RDWR && mode == O_RDWR) ||
+          (required_mode == O_RDONLY && (mode == O_RDWR || mode == O_RDONLY) ||
+           (required_mode == O_WRONLY &&
+            (mode == O_RDWR || mode == O_WRONLY))) &&
+              "invalid file access mode");
+    }
+  }
+#endif
+}
+
+NativeFilePosix::NativeFilePosix(int fd, OpenOptions options,
+                                 bool transfer_ownership)
+    : NativeFileBase(fd, options, transfer_ownership) {}
+
+void NativeFilePosix::CalculateInteractiveAndTerminal() {
+  const int fd = GetDescriptor();
+  if (!File::DescriptorIsValid(fd)) {
+    m_is_interactive = eLazyBoolNo;
+    m_is_real_terminal = eLazyBoolNo;
+    m_supports_colors = eLazyBoolNo;
+    return;
+  }
+  m_is_interactive = eLazyBoolNo;
+  m_is_real_terminal = eLazyBoolNo;
+  if (isatty(fd)) {
+    m_is_interactive = eLazyBoolYes;
+    struct winsize window_size;
+    if (::ioctl(fd, TIOCGWINSZ, &window_size) == 0) {
+      if (window_size.ws_col > 0) {
+        m_is_real_terminal = eLazyBoolYes;
+        if (llvm::sys::Process::FileDescriptorHasColors(fd))
+          m_supports_colors = eLazyBoolYes;
+      }
+    }
+  }
+}
+
+Status NativeFilePosix::GetFileSpec(FileSpec &file_spec) const {
+  Status error;
+#ifdef F_GETPATH
+  if (IsValid()) {
+    char path[PATH_MAX];
+    if (::fcntl(GetDescriptor(), F_GETPATH, path) == -1)
+      error = Status::FromErrno();
+    else
+      file_spec.SetFile(path, FileSpec::Style::native);
+  } else {
+    error = Status::FromErrorString("invalid file handle");
+  }
+#elif defined(__linux__)
+  char proc[64];
+  char path[PATH_MAX];
+  if (::snprintf(proc, sizeof(proc), "/proc/self/fd/%d", GetDescriptor()) < 0)
+    error = Status::FromErrorString("cannot resolve file descriptor");
+  else {
+    ssize_t len;
+    if ((len = ::readlink(proc, path, sizeof(path) - 1)) == -1)
+      error = Status::FromErrno();
+    else {
+      path[len] = '\0';
+      file_spec.SetFile(path, FileSpec::Style::native);
+    }
+  }
+#else
+  error = Status::FromErrorString(
+      "NativeFile::GetFileSpec is not supported on this platform");
+#endif
+
+  if (error.Fail())
+    file_spec.Clear();
+  return error;
+}
+
+Status NativeFilePosix::Read(void *buf, size_t &num_bytes, off_t &offset) {
+  Status error;
+
+#if defined(MAX_READ_SIZE)
+  if (num_bytes > MAX_READ_SIZE) {
+    uint8_t *p = (uint8_t *)buf;
+    size_t bytes_left = num_bytes;
+    // Init the num_bytes read to zero
+    num_bytes = 0;
+
+    while (bytes_left > 0) {
+      size_t curr_num_bytes;
+      if (bytes_left > MAX_READ_SIZE)
+        curr_num_bytes = MAX_READ_SIZE;
+      else
+        curr_num_bytes = bytes_left;
+
+      error = Read(p + num_bytes, curr_num_bytes, offset);
+
+      // Update how many bytes were read
+      num_bytes += curr_num_bytes;
+      if (bytes_left < curr_num_bytes)
+        bytes_left = 0;
+      else
+        bytes_left -= curr_num_bytes;
+
+      if (error.Fail())
+        break;
+    }
+    return error;
+  }
+#endif
+
+  int fd = GetDescriptor();
+  if (fd != kInvalidDescriptor) {
+    ssize_t bytes_read =
+        llvm::sys::RetryAfterSignal(-1, ::pread, fd, buf, num_bytes, offset);
+    if (bytes_read < 0) {
+      num_bytes = 0;
+      error = Status::FromErrno();
+    } else {
+      offset += bytes_read;
+      num_bytes = bytes_read;
+    }
+  } else {
+    num_bytes = 0;
+    error = Status::FromErrorString("invalid file handle");
+  }
+  return error;
+}
+
+Status NativeFilePosix::Write(const void *buf, size_t &num_bytes,
+                              off_t &offset) {
+  Status error;
+
+#if defined(MAX_WRITE_SIZE)
+  if (num_bytes > MAX_WRITE_SIZE) {
+    const uint8_t *p = (const uint8_t *)buf;
+    size_t bytes_left = num_bytes;
+    // Init the num_bytes written to zero
+    num_bytes = 0;
+
+    while (bytes_left > 0) {
+      size_t curr_num_bytes;
+      if (bytes_left > MAX_WRITE_SIZE)
+        curr_num_bytes = MAX_WRITE_SIZE;
+      else
+        curr_num_bytes = bytes_left;
+
+      error = Write(p + num_bytes, curr_num_bytes, offset);
+
+      // Update how many bytes were read
+      num_bytes += curr_num_bytes;
+      if (bytes_left < curr_num_bytes)
+        bytes_left = 0;
+      else
+        bytes_left -= curr_num_bytes;
+
+      if (error.Fail())
+        break;
+    }
+    return error;
+  }
+#endif
+
+  int fd = GetDescriptor();
+  if (fd != kInvalidDescriptor) {
+    ssize_t bytes_written = llvm::sys::RetryAfterSignal(
+        -1, ::pwrite, m_descriptor, buf, num_bytes, offset);
+    if (bytes_written < 0) {
+      num_bytes = 0;
+      error = Status::FromErrno();
+    } else {
+      offset += bytes_written;
+      num_bytes = bytes_written;
+    }
+  } else {
+    num_bytes = 0;
+    error = Status::FromErrorString("invalid file handle");
+  }
+  return error;
+}
diff --git a/lldb/source/Host/windows/FileWindows.cpp 
b/lldb/source/Host/windows/FileWindows.cpp
new file mode 100644
index 0000000000000..f2c788d78a3b6
--- /dev/null
+++ b/lldb/source/Host/windows/FileWindows.cpp
@@ -0,0 +1,139 @@
+//===----------------------------------------------------------------------===//
+//
+// 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 "lldb/Host/windows/FileWindows.h"
+
+#include "lldb/Host/windows/windows.h"
+
+#include <climits>
+#include <io.h>
+#include <mutex>
+#include <stdio.h>
+
+#include "lldb/Utility/Status.h"
+#include "llvm/Support/raw_ostream.h"
+
+using namespace lldb_private;
+
+void NativeFileWindows::DetectIsWindowsConsole(FILE *fh) {
+  // Windows consoles need a special I/O path to render non-ASCII characters
+  // correctly; remember which standard streams are connected to the console.
+  HANDLE h = INVALID_HANDLE_VALUE;
+  if (fh == stdin)
+    h = ::GetStdHandle(STD_INPUT_HANDLE);
+  else if (fh == stdout)
+    h = ::GetStdHandle(STD_OUTPUT_HANDLE);
+  else if (fh == stderr)
+    h = ::GetStdHandle(STD_ERROR_HANDLE);
+  m_is_windows_console =
+      h != INVALID_HANDLE_VALUE && ::GetFileType(h) == FILE_TYPE_CHAR;
+}
+
+void NativeFileWindows::DetectIsWindowsConsole(int fd) {
+  HANDLE h = INVALID_HANDLE_VALUE;
+  if (fd == STDIN_FILENO)
+    h = ::GetStdHandle(STD_INPUT_HANDLE);
+  else if (fd == STDOUT_FILENO)
+    h = ::GetStdHandle(STD_OUTPUT_HANDLE);
+  else if (fd == STDERR_FILENO)
+    h = ::GetStdHandle(STD_ERROR_HANDLE);
+  m_is_windows_console =
+      h != INVALID_HANDLE_VALUE && ::GetFileType(h) == FILE_TYPE_CHAR;
+}
+
+NativeFileWindows::NativeFileWindows(FILE *fh, OpenOptions options,
+                                     bool transfer_ownership)
+    : NativeFileBase(fh, options, transfer_ownership) {
+  DetectIsWindowsConsole(fh);
+}
+
+NativeFileWindows::NativeFileWindows(int fd, OpenOptions options,
+                                     bool transfer_ownership)
+    : NativeFileBase(fd, options, transfer_ownership) {
+  DetectIsWindowsConsole(fd);
+}
+
+void NativeFileWindows::CalculateInteractiveAndTerminal() {
+  const int fd = GetDescriptor();
+  if (!File::DescriptorIsValid(fd)) {
+    m_is_interactive = eLazyBoolNo;
+    m_is_real_terminal = eLazyBoolNo;
+    m_supports_colors = eLazyBoolNo;
+    return;
+  }
+  m_is_interactive = eLazyBoolNo;
+  m_is_real_terminal = eLazyBoolNo;
+  if (_isatty(fd)) {
+    m_is_interactive = eLazyBoolYes;
+    m_is_real_terminal = eLazyBoolYes;
+#if defined(ENABLE_VIRTUAL_TERMINAL_PROCESSING)
+    m_supports_colors = eLazyBoolYes;
+#endif
+  }
+}
+
+int NativeFileWindows::Fileno(FILE *fh) const { return ::_fileno(fh); }
+
+int NativeFileWindows::Dup(int fd) const { return ::_dup(fd); }
+
+IOObject::WaitableHandle NativeFileWindows::GetWaitableHandle() {
+  return (HANDLE)_get_osfhandle(GetDescriptor());
+}
+
+Status NativeFileWindows::Sync() {
+  Status error;
+  if (ValueGuard descriptor_guard = DescriptorIsValid()) {
+    if (FlushFileBuffers((HANDLE)_get_osfhandle(m_descriptor)) == 0)
+      error = Status::FromErrorString("unknown error");
+  } else {
+    error = Status::FromErrorString("invalid file handle");
+  }
+  return error;
+}
+
+bool NativeFileWindows::TryWriteStreamUnlocked(const void *buf,
+                                               size_t &num_bytes,
+                                               Status &error) {
+  if (!m_is_windows_console)
+    return false;
+  // Bypass fwrite for console output: use raw_fd_ostream so that the Windows
+  // console renders non-ASCII characters via its UTF-16 path.
+  llvm::raw_fd_ostream(_fileno(m_stream), false)
+      .write((const char *)buf, num_bytes);
+  return true;
+}
+
+Status NativeFileWindows::Read(void *buf, size_t &num_bytes, off_t &offset) {
+  // Win32 has no pread(); emulate it by saving the current offset, seeking,
+  // reading, and restoring.  Serialize against other positional Reads/Writes
+  // so concurrent callers don't trample the file pointer.
+  std::lock_guard<std::mutex> guard(offset_access_mutex);
+  long cur = ::lseek(m_descriptor, 0, SEEK_CUR);
+  SeekFromStart(offset);
+  Status error = NativeFileBase::Read(buf, num_bytes);
+  if (!error.Fail())
+    SeekFromStart(cur);
+  return error;
+}
+
+Status NativeFileWindows::Write(const void *buf, size_t &num_bytes,
+                                off_t &offset) {
+  // Win32 has no pwrite(); same trick as Read above, but the post-write
+  // file position is what the caller wants reported back via `offset`.
+  std::lock_guard<std::mutex> guard(offset_access_mutex);
+  long cur = ::lseek(m_descriptor, 0, SEEK_CUR);
+  SeekFromStart(offset);
+  Status error = NativeFileBase::Write(buf, num_bytes);
+  long after = ::lseek(m_descriptor, 0, SEEK_CUR);
+
+  if (!error.Fail())
+    SeekFromStart(cur);
+
+  offset = after;
+  return error;
+}

>From 1d20210a7743758074c557d2f7a928d97496f9a4 Mon Sep 17 00:00:00 2001
From: Charles Zablit <[email protected]>
Date: Thu, 7 May 2026 13:19:59 +0100
Subject: [PATCH 2/2] fixup! [lldb] split NativeFile in platform specific
 implementations

---
 lldb/include/lldb/Host/posix/FilePosix.h |  2 ++
 lldb/source/Host/common/File.cpp         | 17 ++++++++---------
 lldb/source/Host/posix/FilePosix.cpp     | 16 ++++++++++++++++
 3 files changed, 26 insertions(+), 9 deletions(-)

diff --git a/lldb/include/lldb/Host/posix/FilePosix.h 
b/lldb/include/lldb/Host/posix/FilePosix.h
index e55d9a0338897..a42a9baddf429 100644
--- a/lldb/include/lldb/Host/posix/FilePosix.h
+++ b/lldb/include/lldb/Host/posix/FilePosix.h
@@ -33,6 +33,8 @@ class NativeFilePosix : public NativeFileBase {
   using NativeFileBase::Write;
 
   Status GetFileSpec(FileSpec &file_spec) const override;
+  WaitableHandle GetWaitableHandle() override;
+  Status Sync() override;
   Status Read(void *dst, size_t &num_bytes, off_t &offset) override;
   Status Write(const void *src, size_t &num_bytes, off_t &offset) override;
 
diff --git a/lldb/source/Host/common/File.cpp b/lldb/source/Host/common/File.cpp
index 0a22c8f949547..07c3ee9d32352 100644
--- a/lldb/source/Host/common/File.cpp
+++ b/lldb/source/Host/common/File.cpp
@@ -256,7 +256,10 @@ int NativeFileBase::GetDescriptor() const {
 }
 
 IOObject::WaitableHandle NativeFileBase::GetWaitableHandle() {
-  return GetDescriptor();
+  // The mapping from descriptor/stream to a poll-able WaitableHandle is
+  // platform-specific (an int on POSIX, a HANDLE on Windows).  Subclasses
+  // override; the base returns the portable invalid sentinel.
+  return IOObject::kInvalidHandleValue;
 }
 
 FILE *NativeFileBase::GetStream() {
@@ -446,14 +449,10 @@ Status NativeFileBase::Flush() {
 }
 
 Status NativeFileBase::Sync() {
-  Status error;
-  if (ValueGuard descriptor_guard = DescriptorIsValid()) {
-    if (llvm::sys::RetryAfterSignal(-1, ::fsync, m_descriptor) == -1)
-      error = Status::FromErrno();
-  } else {
-    error = Status::FromErrorString("invalid file handle");
-  }
-  return error;
+  // Flushing dirty file data to disk uses platform-specific calls
+  // (fsync on POSIX, FlushFileBuffers on Windows).  Subclasses override.
+  return Status::FromErrorString(
+      "Sync is not supported on this NativeFile implementation");
 }
 
 #if defined(__APPLE__)
diff --git a/lldb/source/Host/posix/FilePosix.cpp 
b/lldb/source/Host/posix/FilePosix.cpp
index fb18420435eae..1f70f754aa126 100644
--- a/lldb/source/Host/posix/FilePosix.cpp
+++ b/lldb/source/Host/posix/FilePosix.cpp
@@ -57,6 +57,22 @@ NativeFilePosix::NativeFilePosix(int fd, OpenOptions options,
                                  bool transfer_ownership)
     : NativeFileBase(fd, options, transfer_ownership) {}
 
+IOObject::WaitableHandle NativeFilePosix::GetWaitableHandle() {
+  // POSIX poll/select/epoll work directly on file descriptors.
+  return GetDescriptor();
+}
+
+Status NativeFilePosix::Sync() {
+  Status error;
+  if (ValueGuard descriptor_guard = DescriptorIsValid()) {
+    if (llvm::sys::RetryAfterSignal(-1, ::fsync, m_descriptor) == -1)
+      error = Status::FromErrno();
+  } else {
+    error = Status::FromErrorString("invalid file handle");
+  }
+  return error;
+}
+
 void NativeFilePosix::CalculateInteractiveAndTerminal() {
   const int fd = GetDescriptor();
   if (!File::DescriptorIsValid(fd)) {

_______________________________________________
lldb-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/lldb-commits

Reply via email to