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.
---