https://gcc.gnu.org/bugzilla/show_bug.cgi?id=127342

            Bug ID: 127342
           Summary: std::system_category().default_error_condition should
                    map Winsock WSAE* error codes to the corresponding
                    errno values on MinGW targets
           Product: gcc
           Version: 15.3.0
            Status: UNCONFIRMED
          Severity: enhancement
          Priority: P3
         Component: libstdc++
          Assignee: unassigned at gcc dot gnu.org
          Reporter: awsmadi at amazon dot com
  Target Milestone: ---

Description
===========

This is an enhancement request, not a conformance defect.
[syserr.errcat.objects] leaves it
unspecified what constitutes correspondence between a system error value and a
POSIX errno
value  --  "What constitutes correspondence for any given operating system is
unspecified"  --  so
returning the system category for WSAE* codes is permitted. The request is that
MinGW targets
treat the Winsock range as corresponding, the same way PR 60555 made the Win32
ERROR_* range
correspond, and the same way MSVC's STL already does.

The practical consequence is that the portable spelling of a socket error check
silently takes the
wrong branch on MinGW. Code like

  if (ec == std::errc::operation_would_block)
      return {};                    // nothing to read yet, try again later

  if (ec == std::errc::connection_reset)
      dropConnection();             // peer went away


compiles, runs, and takes neither branch; the error escapes instead. There is
no diagnostic, and
ec.message() still returns correct human-readable text, which makes it easy to
miss in review  -- 
a sensible message reads as a working error_code.

WSAEWOULDBLOCK is the case that matters most in practice, since every
non-blocking socket
produces it on the normal path.

The gap is specific to the WSAE* range, not to socket errors generally: ERROR_*
codes with
socket meanings are already mapped. system_error.cc:223 has
X (CONNECTION_REFUSED, ECONNREFUSED); and ERROR_CONNECTION_REFUSED is 1225, so
std::error_code(1225, std::system_category()) == std::errc::connection_refused
is already true.

Mechanism
=========

In libstdc++-v3/src/c++11/system_error.cc,
system_error_category::default_error_condition
builds its Windows mapping with

  #define X(w, e) case ERROR_##w: return {e, generic_category_instance.obj};


The ERROR_ prefix is token-pasted by the macro, so a WSAE* constant cannot be
expressed through
this table at all  --  the omission is structural rather than a missing entry.
The accompanying comment
says the list is "based on Cygwin's winsup/cygwin/errno.cc". That lineage may
explain the scope:
Cygwin's Win32 table (winsup/cygwin/local_includes/errmap.h) holds 2209 ERROR_
entries and no
Winsock entries, because Cygwin translates Winsock errors in a separate layer.

Codes outside the table reach

  default: return std::error_condition(ev, *this);


which returns the raw value in the *system* category rather than a generic
condition. Since
equivalent(int, const error_condition&) is overridden to
default_error_condition(i) == cond,
comparing against std::errc::interrupted evaluates
{10004, system_category} == {EINTR, generic_category}, which is false.

The file already anticipates additions of this kind, at lines 572-576:

        /* Additional system-dependent mappings from non-standard error codes
         * to one of the POSIX values above would go here, e.g.
        case EBLAH:
        return std::error_condition(EINVAL, std::generic_category());
         */


Separately, config/os/mingw32-w64/error_constants.h lines 40-41 carry the
observation "Most of the
commented-out error codes are socket-related and could be replaced by Winsock
WSA-prefixed
equivalents." (The codes it referred to were subsequently defined by PR 71444,
so nothing is
commented out in that file today; the remark is cited only as prior recognition
of this gap, not as
a list to work from.)

Reproducer
==========

  // Does libstdc++ map Winsock error codes into std::errc on MinGW?
  //
  // Builds the error_code the way code typically does after WSAGetLastError --
  // std::error_code(value, std::system_category()) -- and compares against the
  // std::errc value a POSIX program would test for.
  //
  // No Winsock function is called, only its constants are read, so there is no
  // WSAStartup and no need to link ws2_32.

  #include <cstdio>
  #include <initializer_list>
  #include <system_error>

  #ifdef _WIN32
  #  include <winsock2.h>
  #else
  #  include <cerrno>
  #endif

  namespace {

  struct Case
  {
      const char * name;
      int value;
      std::errc expected;
      const char * expectedName;
  };

  } // namespace

  int main()
  {
  #ifdef _WIN32
      const char * platform = "WINDOWS (mingw/libstdc++)";
      const Case cases[] = {
          {"WSAEINTR", WSAEINTR, std::errc::interrupted, "interrupted"},
          {"WSAEINVAL", WSAEINVAL, std::errc::invalid_argument,
"invalid_argument"},
          {"WSAECONNABORTED", WSAECONNABORTED, std::errc::connection_aborted,
"connection_aborted"},
          {"WSAENOTSOCK", WSAENOTSOCK, std::errc::not_a_socket,
"not_a_socket"},
          {"WSAEWOULDBLOCK", WSAEWOULDBLOCK, std::errc::operation_would_block,
"operation_would_block"},
      };
  #else
      const char * platform = "POSIX (glibc/libstdc++) control";
      const Case cases[] = {
          {"EINTR", EINTR, std::errc::interrupted, "interrupted"},
          {"EINVAL", EINVAL, std::errc::invalid_argument, "invalid_argument"},
          {"ECONNABORTED", ECONNABORTED, std::errc::connection_aborted,
"connection_aborted"},
          {"ENOTSOCK", ENOTSOCK, std::errc::not_a_socket, "not_a_socket"},
          {"EWOULDBLOCK", EWOULDBLOCK, std::errc::operation_would_block,
"operation_would_block"},
      };
  #endif

      std::printf("platform: %s\n", platform);
      std::printf("%-18s %7s  %-22s %-6s %s\n", "code", "value", "compared
against", "equal", "default_error_condition");

      int mapped = 0;
      const int total = int(sizeof(cases) / sizeof(cases[0]));

      for (const Case & c : cases) {
          const std::error_code ec(c.value, std::system_category());
          const bool eq = (ec == c.expected);
          if (eq)
              ++mapped;
          const std::error_condition cond = ec.default_error_condition();
          std::printf(
              "%-18s %7d  %-22s %-6s cat=%s val=%d\n",
              c.name,
              c.value,
              c.expectedName,
              eq ? "TRUE" : "FALSE",
              cond.category().name(),
              cond.value());
      }

      std::printf("\nmapped %d of %d\n", mapped, total);

      /* A readable message does not imply a working comparison; print both so
the
         two cannot be conflated. */
      for (int i : {0, 2})
          std::printf(
              "message(%d) = \"%s\"\n",
              cases[i].value,
              std::error_code(cases[i].value,
std::system_category()).message().c_str());

      return 0;
  }


Built with x86_64-w64-mingw32-g++ -std=c++20 -O0 -static repro.cc -o repro.exe.
No WSAStartup
and no -lws2_32: the program calls no Winsock function, it only reads the
constants. The
error_code is constructed the way user code does after WSAGetLastError, through
std::error_code(value, std::system_category()) rather than make_error_code.

Output on Windows
=================

  platform: WINDOWS (mingw/libstdc++)
  code                 value  compared against       equal 
default_error_condition
  WSAEINTR             10004  interrupted            FALSE  cat=system
val=10004
  WSAEINVAL            10022  invalid_argument       FALSE  cat=system
val=10022
  WSAECONNABORTED      10053  connection_aborted     FALSE  cat=system
val=10053
  WSAENOTSOCK          10038  not_a_socket           FALSE  cat=system
val=10038
  WSAEWOULDBLOCK       10035  operation_would_block  FALSE  cat=system
val=10035

  mapped 0 of 5
  message(10004) = "A blocking operation was interrupted by a call to
WSACancelBlockingCall"
  message(10053) = "An established connection was aborted by the software in
your host machine"


Output from the same source built for a POSIX target
====================================================

  platform: POSIX (glibc/libstdc++) control
  code                 value  compared against       equal 
default_error_condition
  EINTR                    4  interrupted            TRUE   cat=generic val=4
  EINVAL                  22  invalid_argument       TRUE   cat=generic val=22
  ECONNABORTED           103  connection_aborted     TRUE   cat=generic val=103
  ENOTSOCK                88  not_a_socket           TRUE   cat=generic val=88
  EWOULDBLOCK             11  operation_would_block  TRUE   cat=generic val=11

  mapped 5 of 5
  message(4) = "Interrupted system call"
  message(103) = "Software caused connection abort"


The control establishes that the harness and the comparison logic are sound, so
a FALSE above is
a mapping gap rather than a broken test. Note the two message() lines on
Windows: the codes are
recognized well enough to format, while still comparing unequal.

The outcome does not depend on the environment. No OS call participates on the
comparison path:
name() returns string literals (136, 155), default_error_condition (189-582) is
a switch over
integer constants, and equivalent (585-587) delegates to it. The only
OS-touching call the
reproducer reaches is message(), via FormatMessageA, and nothing in the
comparison consults it.

Comparison with MSVC
====================

Microsoft's STL maps all five. stl/src/syserror.cpp carries them in the table
consumed by
_Winerror_map  --  {WSAEINTR, errc::interrupted}, {WSAEINVAL,
errc::invalid_argument},
{WSAEWOULDBLOCK, errc::operation_would_block}, {WSAENOTSOCK,
errc::not_a_socket},
{WSAECONNABORTED, errc::connection_aborted}  --  so identical source takes the
correct branch there
and the wrong one under libstdc++. Portable code cannot rely on the comparison,
and the divergence
appears on only one Windows toolchain.

Suggested change
================

Extend the Windows arm of default_error_condition with the Winsock range.
Because the existing X
macro hardcodes the ERROR_ prefix, this needs either a second macro taking a
fully spelled
constant, or entries written outside the macro.

  Winsock code     errno
  ---------------  ------------
  WSAEINTR         EINTR
  WSAEINVAL        EINVAL
  WSAEWOULDBLOCK   EWOULDBLOCK
  WSAENOTSOCK      ENOTSOCK
  WSAECONNABORTED  ECONNABORTED
  WSAECONNRESET    ECONNRESET
  WSAECONNREFUSED  ECONNREFUSED
  WSAETIMEDOUT     ETIMEDOUT
  WSAEADDRINUSE    EADDRINUSE
  WSAEHOSTUNREACH  EHOSTUNREACH
  WSAENETUNREACH   ENETUNREACH
  WSAEMSGSIZE      EMSGSIZEEvery row agrees with both existing reference
implementations: Cygwin's wsock_errmap[]
(winsup/cygwin/net.cc, line 142), a dense array indexed by WSAE* - WSABASEERR
and read by
find_winsock_errno(), covering 43 codes; and MSVC's _Win_errtab. Cygwin's table
is a usable
source for the errno values to copy, though it says nothing about what
libstdc++ should treat as
corresponding.

The twelve above are the codes portable socket code actually tests, but the
scope is yours to
choose rather than mine: MSVC maps 31 WSAE* codes in total. The nineteen not
listed are
WSAEACCES, WSAEADDRNOTAVAIL, WSAEAFNOSUPPORT, WSAEALREADY, WSAEBADF,
WSAEDESTADDRREQ,
WSAEFAULT, WSAEINPROGRESS, WSAEISCONN, WSAEMFILE, WSAENAMETOOLONG, WSAENETDOWN,
WSAENETRESET, WSAENOBUFS, WSAENOPROTOOPT, WSAENOTCONN, WSAEOPNOTSUPP,
WSAEPROTONOSUPPORT and WSAEPROTOTYPE, if the wider set is preferred.

The new labels cannot collide with the existing ones. Resolving all 108 X()
names in the switch
against winerror.h gives a maximum of ERROR_DS_GENERIC_ERROR = 8341, below
WSABASEERR = 10000,
so the Winsock range is disjoint from every value already handled.

Two rows need guards, matching how the same enumerators are guarded in
config/os/mingw32-w64/error_constants.h: operation_would_block at lines 125-127
(#ifdef EWOULDBLOCK) and timed_out at 147-149 (#ifdef ETIMEDOUT). The other ten
enumerators are
unguarded there. On x86_64-w64-mingw32 both macros are in fact defined  -- 
EWOULDBLOCK 140,
ETIMEDOUT 138, EAGAIN 11  --  so the guards do not exclude them on this target.

A plain #ifdef EWOULDBLOCK suffices in the Windows arm. The stricter
#if defined EWOULDBLOCK && (!defined EAGAIN || EWOULDBLOCK != EAGAIN) at line
563 guards a case
label, and the arms are mutually exclusive  --  #if defined(_WIN32) &&
!defined(__CYGWIN__) at 196,
#elif defined __AVR__ at 321, #else at 330, #endif at 578  --  so case
EWOULDBLOCK: at 564 is
not compiled on Windows at all, and a duplicate label cannot arise there
regardless of whether
EWOULDBLOCK and EAGAIN are equal.

Testsuite
=========

libstdc++-v3/testsuite/19_diagnostics/error_category/system_category.cc already
has a
#if defined __MINGW32__ || defined __MINGW64__ block (line 24) that checks
default_error_condition(8) for ERROR_NOT_ENOUGH_MEMORY (line 43) and
default_error_condition(5)
for ERROR_ACCESS_DENIED (line 48). That block ends in an early return; at line
52, so new
WSAE* assertions have to be added before it or they will not execute.

---

Reply via email to