Skip to content

feat(win): Add non-blocking overlapped I/O implementation for Windows#19

Open
lrodorigo-versuni wants to merge 13 commits into
gbionics:mainfrom
lrodorigo-versuni:feature/win-overlapped-io
Open

feat(win): Add non-blocking overlapped I/O implementation for Windows#19
lrodorigo-versuni wants to merge 13 commits into
gbionics:mainfrom
lrodorigo-versuni:feature/win-overlapped-io

Conversation

@lrodorigo-versuni

Copy link
Copy Markdown

Problem

The Linux/Unix implementation uses select()/poll() under the hood, which naturally allows concurrent read and write operations from different threads. A reader thread waiting for data does not block a writer thread from sending data — they are fully independent.

The Windows implementation, on the other hand, uses synchronous ReadFile/WriteFile (opened with FILE_ATTRIBUTE_NORMAL and NULL overlapped parameter). This means read and write serialize on the same handle: if a thread is blocked in ReadFile waiting for incoming data, any other thread calling WriteFile will be stuck until the read times out.

This behavioral difference between Linux and Windows drove me absolutely insane. Code that worked perfectly on Linux — with a reader thread continuously polling for data while a writer thread sends commands — would completely deadlock on Windows. The writer would freeze for the entire duration of the read timeout, making real-time bidirectional communication impossible. I spent way too long debugging this before realizing it was a fundamental limitation of the synchronous Win32 serial API.

Solution

This PR adds an alternative Windows implementation using overlapped (asynchronous) I/O (FILE_FLAG_OVERLAPPED), selectable via a CMake option:

  • No breaking changes: The original win.cc/win.h are untouched. Default is SERIAL_CPP_WIN_ASYNC=OFF but can be turned ON to enable the new non-blocking behavior.
  • WaitCommEvent mask is a class member: Windows writes to the lpEvtMask DWORD asynchronously after WaitCommEvent returns
  • waitReadable() and waitByteTimes() now work on Windows (previously they just threw "not implemented").
option(SERIAL_CPP_WIN_ASYNC "Use overlapped (async) I/O on Windows" OFF)

New Test program

I also added a small test program (examples/serial_cpp_async_test.cc) that demonstrates the issue and verifies the fix. It spawns a reader thread and a writer thread operating concurrently on the same serial port. With the old blocking implementation the writer would stall for the entire read timeout; with SERIAL_CPP_WIN_ASYNC=ON both threads proceed independently, as expected.

./serial_cpp_async_test COM3 115200

Usage

cmake -B build -DSERIAL_CPP_WIN_ASYNC=ON
cmake --build build

This makes the Windows behavior match Linux: read and write from separate threads operate independently without blocking each other.


- Open port with FILE_FLAG_OVERLAPPED instead of FILE_ATTRIBUTE_NORMAL
- Use OVERLAPPED structs with events for ReadFile/WriteFile
- Handle ERROR_IO_PENDING + WaitForSingleObject for proper timeout
- Implement waitReadable() using WaitCommEvent with overlapped I/O
- Implement waitByteTimes() with calculated sleep
- Use overlapped WaitCommEvent in waitForChange()
- Cancel pending I/O on close()

This allows concurrent read/write from different threads without
blocking. Previously, a write during a pending read would block
until the read timeout expired.
When -DSERIAL_CPP_WIN_ASYNC=ON is passed to CMake, the library will
be built with the overlapped (async) Windows implementation that
allows concurrent read/write from different threads.

Default is OFF, preserving the original blocking behavior.
…PPED/stack pointers

Key fixes:
- Use local OVERLAPPED structs on the stack but ALWAYS wait for
  completion (GetOverlappedResult with bWait=TRUE) or cancel+wait
  before leaving scope. This ensures Windows never writes to freed memory.
- Use CancelIoEx instead of CancelIo (targets specific overlapped op).
- Make wait_comm_event_mask_ a class member since WaitCommEvent writes
  to it asynchronously after the call returns.
- Use separate local event for waitForChange() to avoid conflicts.
- Properly handle ERROR_OPERATION_ABORTED after cancellation.
- Initialize events in constructor before open() to avoid use-before-init.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant