Description
The following seemingly innocuous code:
WinError.value = WinError::ERROR_INVALID_PARAMETER
GC.malloc(1)
p WinError.value
prints WinError::ERROR_INVALID_PARAMETER
for MSVC, but WinError::ERROR_SUCCESS
for MinGW-w64. The reason is rather subtle:
- Boehm GC on Windows uses different ways to access thread-local storage depending on whether a GNU compiler is used; it defines
USE_WIN32_SPECIFIC
orUSE_WIN32_COMPILER_TLS
for GNU and MSVC respectively. - For MSVC it uses
__declspec(thread)
, and for GNU it uses the internalGC_getspecific
, which is a macro for the Win32TlsGetValue
. TlsGetValue
is one of the few Win32 functions that always set a thread's last error, even if it succeeded.- This line is where
GC_malloc
orGC_malloc_atomic
ultimately accesses TLS.
Few allocations look as straightforward as the code above. Here is one:
crystal/src/crystal/system/win32/socket.cr
Lines 169 to 173 in 181196f
WSAGetLastError
and GetLastError
are identical, except for their return types. Since this string interpolation allocates memory before the body of SystemError::ClassMethods#from_wsa_error
calls WinError.wsa_value
, the error message will always be "The operation completed successfully.".
One might argue that this is sensible behavior, in that Errno.value
shares the same caveat with respect to C functions. But while functions in POSIX or the C standard library, including malloc
, may write positive integers to errno
regardless of whether a failure occurred, they never write 0. Indeed, if the top snippet prints neither ERROR_INVALID_PARAMETER
nor ERROR_SUCCESS
, then a genuine error might have happened somewhere else. ERROR_SUCCESS
falls out of this natural expectation.
We could work around this by defining an appropriate Socket::Error.build_message
, but I wonder if there is a better solution for the general case, other than preserving WinError.value
on every single dynamic allocation.