SDL_net: First shot at an SDL3_net API and implementation! WIP!

From b595e79a4f6c5531c4385b92f8da2057dcb85a7d Mon Sep 17 00:00:00 2001
From: "Ryan C. Gordon" <[EMAIL REDACTED]>
Date: Tue, 18 Jul 2023 19:44:50 -0400
Subject: [PATCH] First shot at an SDL3_net API and implementation! WIP!

---
 CMakeLists.txt |  288 +---------
 README.md      |   19 +
 README.txt     |   26 -
 SDL_net.c      | 1381 ++++++++++++++++++++++++++++++++++++++++++++++++
 SDL_net.h      | 1103 ++------------------------------------
 5 files changed, 1456 insertions(+), 1361 deletions(-)
 create mode 100644 README.md
 delete mode 100644 README.txt
 create mode 100644 SDL_net.c

diff --git a/CMakeLists.txt b/CMakeLists.txt
index 17dfb9f..012731d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -1,288 +1,4 @@
 cmake_minimum_required(VERSION 3.16)
+project(SDL3_net)
+add_library(SDL3_net SHARED SDL_net.c)
 
-list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_LIST_DIR}/cmake")
-
-# See release_checklist.md
-set(MAJOR_VERSION 2)
-set(MINOR_VERSION 3)
-set(MICRO_VERSION 0)
-set(SDL_REQUIRED_VERSION 2.0.4)
-
-include(PrivateSdlFunctions)
-sdl_calculate_derived_version_variables()
-
-if(CMAKE_SOURCE_DIR STREQUAL CMAKE_BINARY_DIR)
-    message(FATAL_ERROR "Prevented in-tree built. Please create a build directory outside of the SDL_net source code and call cmake from there")
-endif()
-
-project(SDL2_net
-    LANGUAGES C
-    VERSION "${FULL_VERSION}"
-)
-
-message(STATUS "Configuring ${PROJECT_NAME} ${PROJECT_VERSION}")
-
-if(CMAKE_SOURCE_DIR STREQUAL PROJECT_SOURCE_DIR)
-    set(SDL2NET_ROOTPROJECT ON)
-else()
-    set(SDL2NET_ROOTPROJECT OFF)
-endif()
-
-if(TARGET SDL2::SDL2test)
-    set(SDL2test_FOUND ON)
-else()
-    find_package(SDL2test)
-endif()
-
-set(SDLNET_PKGCONFIG_PRIVATE_LIBS)
-
-# Set defaults preventing destination file conflicts
-set(SDL2NET_DEBUG_POSTFIX "d"
-    CACHE STRING "Name suffix for debug builds")
-mark_as_advanced(SDL2NET_DEBUG_POSTFIX)
-
-set(sdl2net_install_enableable ON)
-if ((TARGET SDL2 OR TARGET SDL2-static) AND SDL2_DISABLE_INSTALL)
-    # Cannot install SDL2_net when SDL2 is built in same built, and is not installed.
-    set(sdl2net_install_enableable OFF)
-endif()
-
-include(CMakeDependentOption)
-include(CMakePackageConfigHelpers)
-include(GNUInstallDirs)
-
-option(CMAKE_POSITION_INDEPENDENT_CODE "Build static libraries with -fPIC" ON)
-option(BUILD_SHARED_LIBS "Build the library as a shared library" ON)
-
-cmake_dependent_option(SDL2NET_INSTALL "Enable SDL2_net install target" ${SDL2NET_ROOTPROJECT} ${sdl2net_install_enableable} OFF)
-
-cmake_dependent_option(SDL2NET_SAMPLES "Build SDL2_net samples" ${SDL2NET_ROOTPROJECT} "${SDL2test_FOUND}" OFF)
-
-if(SDL2NET_SAMPLES)
-    find_package(SDL2test REQUIRED)
-endif()
-
-# Save BUILD_SHARED_LIBS variable
-set(SDL2NET_BUILD_SHARED_LIBS ${BUILD_SHARED_LIBS})
-
-if(SDL2NET_BUILD_SHARED_LIBS)
-    set(sdl2_net_export_name SDL2_net)
-    set(sdl2_net_install_name_infix shared)
-    set(sdl2_target_name SDL2::SDL2)
-else()
-    set(sdl2_net_export_name SDL2_net-static)
-    set(sdl2_net_install_name_infix static)
-    set(sdl2_target_name SDL2::SDL2-static)
-endif()
-
-sdl_find_sdl2(${sdl2_target_name} ${SDL_REQUIRED_VERSION})
-
-add_library(SDL2_net
-    SDLnet.c
-    SDLnetselect.c
-    SDLnetTCP.c
-    SDLnetUDP.c
-)
-add_library(SDL2_net::${sdl2_net_export_name} ALIAS SDL2_net)
-target_include_directories(SDL2_net PUBLIC
-    "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
-    "$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}/SDL2>"
-)
-target_compile_definitions(SDL2_net PRIVATE
-    BUILD_SDL
-    SDL_BUILD_MAJOR_VERSION=${MAJOR_VERSION}
-    SDL_BUILD_MINOR_VERSION=${MINOR_VERSION}
-    SDL_BUILD_MICRO_VERSION=${MICRO_VERSION}
-)
-target_link_libraries(SDL2_net PRIVATE $<BUILD_INTERFACE:${sdl2_target_name}>)
-if(WIN32)
-    if (MSVC)
-        target_compile_options(SDL2_net PRIVATE /W3 /wd4244)
-    endif()
-    target_compile_definitions(SDL2_net PRIVATE _WINSOCK_DEPRECATED_NO_WARNINGS)
-    target_link_libraries(SDL2_net PRIVATE ws2_32 iphlpapi)
-    list(APPEND SDLNET_PKGCONFIG_PRIVATE_LIBS -lws2_32 -liphlpapi)
-    if(SDL2NET_BUILD_SHARED_LIBS)
-        target_sources(SDL2_net PRIVATE version.rc)
-    endif()
-endif()
-set_target_properties(SDL2_net PROPERTIES
-    DEFINE_SYMBOL DLL_EXPORT
-    EXPORT_NAME ${sdl2_net_export_name}
-    C_VISIBILITY_PRESET "hidden"
-)
-if(NOT ANDROID)
-    set_target_properties(SDL2_net PROPERTIES
-        DEBUG_POSTFIX "${SDL2NET_DEBUG_POSTFIX}"
-    )
-    if(APPLE)
-        # the SOVERSION property corresponds to the compatibility version and VERSION corresponds to the current version
-        # https://cmake.org/cmake/help/latest/prop_tgt/SOVERSION.html#mach-o-versions
-        set_target_properties(SDL2_net PROPERTIES
-            SOVERSION "${DYLIB_COMPATIBILITY_VERSION}"
-            VERSION "${DYLIB_CURRENT_VERSION}"
-        )
-    else()
-        set_target_properties(SDL2_net PROPERTIES
-            SOVERSION "${LT_MAJOR}"
-            VERSION "${LT_VERSION}"
-        )
-    endif()
-endif()
-if(SDL2NET_BUILD_SHARED_LIBS AND (APPLE OR (UNIX AND NOT ANDROID)))
-    add_custom_command(TARGET SDL2_net POST_BUILD
-        COMMAND "${CMAKE_COMMAND}" -E create_symlink "$<TARGET_SONAME_FILE_NAME:SDL2_net>" "libSDL2_net$<$<CONFIG:Debug>:${SDL2NET_DEBUG_POSTFIX}>$<TARGET_FILE_SUFFIX:SDL2_net>"
-        WORKING_DIRECTORY "${PROJECT_BINARY_DIR}"
-    )
-endif()
-if(SDL2NET_BUILD_SHARED_LIBS)
-    if(WIN32 OR OS2)
-        set_target_properties(SDL2_net PROPERTIES
-            PREFIX ""
-        )
-    endif()
-    if(OS2)
-        # FIXME: OS/2 Makefile has a different LIBNAME
-        set_target_properties(SDL2_net PROPERTIES
-            OUTPUT_NAME "SDL2net"
-        )
-    elseif(UNIX AND NOT ANDROID)
-        set_target_properties(SDL2_net PROPERTIES
-            OUTPUT_NAME "SDL2_net-${LT_RELEASE}"
-        )
-    endif()
-else()
-    if(MSVC OR (WATCOM AND (WIN32 OR OS2)))
-        set_target_properties(SDL2_net PROPERTIES
-            OUTPUT_NAME "SDL2_net-static"
-        )
-    endif()
-endif()
-
-# Use `Compatible Interface Properties` to ensure a shared SDL2_net is built with a shared SDL2
-if(SDL2NET_BUILD_SHARED_LIBS)
-    set_target_properties(SDL2_net PROPERTIES 
-        INTERFACE_SDL2_SHARED ${SDL2NET_BUILD_SHARED_LIBS}
-        COMPATIBLE_INTERFACE_BOOL SDL2_SHARED
-    )
-endif()
-
-if(SDL2NET_BUILD_SHARED_LIBS)
-    sdl_target_link_options_no_undefined(SDL2_net)
-endif()
-
-if(SDL2NET_INSTALL)
-    install(
-        TARGETS SDL2_net
-        EXPORT SDL2NetExports
-        ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT devel
-        LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" COMPONENT library
-        RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}" COMPONENT library
-    )
-    install(FILES
-        "${CMAKE_CURRENT_SOURCE_DIR}/SDL_net.h"
-        DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}/SDL2" COMPONENT devel
-    )
-
-    if(WIN32 AND NOT MINGW)
-        set(SDLNET_INSTALL_CMAKEDIR_DEFAULT "cmake")
-    else()
-        set(SDLNET_INSTALL_CMAKEDIR_DEFAULT "${CMAKE_INSTALL_LIBDIR}/cmake/SDL2_net")
-    endif()
-    set(SDLNET_INSTALL_CMAKEDIR "${SDLNET_INSTALL_CMAKEDIR_DEFAULT}" CACHE STRING "Location where to install SDL2_netConfig.cmake")
-
-    configure_package_config_file(SDL2_netConfig.cmake.in SDL2_netConfig.cmake
-        INSTALL_DESTINATION "${SDLNET_INSTALL_CMAKEDIR}"
-    )
-    write_basic_package_version_file("${PROJECT_BINARY_DIR}/SDL2_netConfigVersion.cmake"
-        VERSION ${FULL_VERSION}
-        COMPATIBILITY AnyNewerVersion
-    )
-    install(
-        FILES
-            "${CMAKE_CURRENT_BINARY_DIR}/SDL2_netConfig.cmake"
-            "${CMAKE_CURRENT_BINARY_DIR}/SDL2_netConfigVersion.cmake"
-        DESTINATION "${SDLNET_INSTALL_CMAKEDIR}"
-        COMPONENT devel
-    )
-    install(EXPORT SDL2NetExports
-        FILE SDL2_net-${sdl2_net_install_name_infix}-targets.cmake
-        NAMESPACE SDL2_net::
-        DESTINATION "${SDLNET_INSTALL_CMAKEDIR}"
-        COMPONENT devel
-    )
-
-    if(SDL2NET_BUILD_SHARED_LIBS)
-        set(ENABLE_SHARED_TRUE "")
-        set(ENABLE_SHARED_FALSE "#")
-        set(ENABLE_STATIC_TRUE "#")
-        set(ENABLE_STATIC_FALSE "")
-    else()
-        set(ENABLE_SHARED_TRUE "#")
-        set(ENABLE_SHARED_FALSE "")
-        set(ENABLE_STATIC_TRUE "")
-        set(ENABLE_STATIC_FALSE "#")
-    endif()
-
-    set(prefix "${CMAKE_INSTALL_PREFIX}")
-    set(exec_prefix "\${prefix}")
-    set(libdir "\${exec_prefix}/${CMAKE_INSTALL_LIBDIR}")
-    set(includedir "\${prefix}/${CMAKE_INSTALL_INCLUDEDIR}")
-    set(PACKAGE "${PROJECT_NAME}")
-    set(VERSION "${FULL_VERSION}")
-    set(SDL_VERSION "${SDL_REQUIRED_VERSION}")
-    string(JOIN " " PC_REQUIRES ${PC_REQUIRES})
-    string(JOIN " " PC_LIBS ${PC_LIBS})
-    string(JOIN " " INETLIB ${SDLNET_PKGCONFIG_PRIVATE_LIBS})
-    configure_file("${PROJECT_SOURCE_DIR}/SDL2_net.pc.in" "${CMAKE_CURRENT_BINARY_DIR}/SDL2_net.pc.intermediate" @ONLY)
-    file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/SDL2_net-$<CONFIG>.pc" INPUT "${CMAKE_CURRENT_BINARY_DIR}/SDL2_net.pc.intermediate")
-
-    set(PC_DESTDIR)
-    if(CMAKE_SYSTEM_NAME MATCHES FreeBSD)
-        # FreeBSD uses ${PREFIX}/libdata/pkgconfig
-        set(PC_DESTDIR "libdata/pkgconfig")
-    else()
-        set(PC_DESTDIR "${CMAKE_INSTALL_LIBDIR}/pkgconfig")
-    endif()
-    # Always install SDL2_net.pc: libraries might be different between config modes
-    install(CODE "
-        # FIXME: use file(COPY_FILE) if minimum CMake version >= 3.21
-        execute_process(COMMAND \"\${CMAKE_COMMAND}\" -E copy_if_different
-            \"${CMAKE_CURRENT_BINARY_DIR}/SDL2_net-$<CONFIG>.pc\"
-            \"${CMAKE_CURRENT_BINARY_DIR}/SDL2_net.pc\")
-        file(INSTALL DESTINATION \"\${CMAKE_INSTALL_PREFIX}/${PC_DESTDIR}\"
-            TYPE FILE
-            FILES \"${CMAKE_CURRENT_BINARY_DIR}/SDL2_net.pc\")" COMPONENT devel)
-
-    if(SDL2NET_BUILD_SHARED_LIBS AND (APPLE OR (UNIX AND NOT ANDROID)))
-        install(
-            FILES
-                "${PROJECT_BINARY_DIR}/libSDL2_net$<$<CONFIG:Debug>:${SDL2NET_DEBUG_POSTFIX}>$<TARGET_FILE_SUFFIX:SDL2_net>"
-            DESTINATION "${CMAKE_INSTALL_LIBDIR}"
-            COMPONENT devel
-        )
-    endif()
-
-    install(FILES "LICENSE.txt"
-        DESTINATION "${CMAKE_INSTALL_DATAROOTDIR}/licenses/${PROJECT_NAME}"
-        COMPONENT library
-    )
-endif()
-
-if(SDL2NET_SAMPLES)
-    find_package(SDL2main)
-
-    add_executable(showinterfaces showinterfaces.c)
-    target_compile_definitions(showinterfaces PRIVATE SDL_MAIN_HANDLED)
-    target_link_libraries(showinterfaces PRIVATE SDL2_net::${sdl2_net_export_name} ${sdl2_target_name})
-
-    add_executable(chat chat.c chat.h)
-    if(TARGET SDL2::SDL2main)
-        target_link_libraries(chat PRIVATE SDL2::SDL2main)
-    endif()
-    target_link_libraries(chat PRIVATE SDL2_net::${sdl2_net_export_name} SDL2::SDL2test ${sdl2_target_name})
-
-    add_executable(chatd chatd.c)
-    target_compile_definitions(chatd PRIVATE SDL_MAIN_HANDLED)
-    target_link_libraries(chatd PRIVATE SDL2_net::${sdl2_net_export_name} ${sdl2_target_name})
-endif()
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..4ce286a
--- /dev/null
+++ b/README.md
@@ -0,0 +1,19 @@
+# SDL_net 3.0
+
+The latest version of this library is available from GitHub:
+
+https://github.com/libsdl-org/SDL_net/releases
+
+This is a portable network library for use with SDL. It's goal is to
+simplify the use of the usual socket interfaces and use SDL infrastructure
+to handle some portability things (such as threading and reporting
+errors).
+
+It is available under the zlib license, found in the file LICENSE.txt.
+The API can be found in the file SDL_net.h and online at https://wiki.libsdl.org/SDL3_net
+This library supports most platforms that offer both SDL3 and networking.
+
+This is a work in progress!
+
+Enjoy!
+
diff --git a/README.txt b/README.txt
deleted file mode 100644
index d5e7f0a..0000000
--- a/README.txt
+++ /dev/null
@@ -1,26 +0,0 @@
-
-SDL_net 2.0
-
-The latest version of this library is available from GitHub:
-https://github.com/libsdl-org/SDL_net/releases
-
-This is an example portable network library for use with SDL.
-It is available under the zlib license, found in the file LICENSE.txt.
-The API can be found in the file SDL_net.h and online at https://wiki.libsdl.org/SDL_net
-This library supports UNIX, Windows, MacOS Classic, MacOS X,
-BeOS and QNX.
-
-The demo program is a chat client and server.
-
-The chat client connects to the server via TCP, registering itself.
-The server sends back a list of connected clients, and keeps the
-client updated with the status of other clients.
-Every line of text from a client is sent via UDP to every other client.
-
-Note that this isn't necessarily how you would want to write a chat
-program, but it demonstrates how to use the basic features of the 
-network library.
-
-Enjoy!
-	-Sam Lantinga and Roy Wood
-
diff --git a/SDL_net.c b/SDL_net.c
new file mode 100644
index 0000000..4710892
--- /dev/null
+++ b/SDL_net.c
@@ -0,0 +1,1381 @@
+#include "SDL_net.h"
+
+#ifdef __WINDOWS__
+#define WIN32_LEAN_AND_MEAN 1
+#include <windows.h>
+#include <winsock.h>
+typedef SOCKET Socket;
+typedef int SockLen;
+typedef SOCKADDR_STORAGE AddressStorage;
+#else
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#include <errno.h>
+#include <string.h>
+#include <signal.h>
+#include <poll.h>
+#include <unistd.h>
+#include <fcntl.h>
+#define INVALID_SOCKET -1
+#define SOCKET_ERROR -1
+typedef int Socket;
+typedef socklen_t SockLen;
+typedef struct sockaddr_storage AddressStorage;
+#endif
+
+const SDL_version *SDLNet_Linked_Version(void)
+{
+    static const SDL_version linked_version = {
+        SDL_NET_MAJOR_VERSION, SDL_NET_MINOR_VERSION, SDL_NET_PATCHLEVEL
+    };
+    return &linked_version;
+}
+
+struct SDLNet_Address
+{
+    char *hostname;
+    char *human_readable;
+    char *errstr;
+    SDL_AtomicInt refcount;
+    SDL_AtomicInt status;  // 0==in progress, 1==resolved, 2==error
+    struct addrinfo *ainfo;
+    SDLNet_Address *resolver_next;  // a linked list for the resolution job queue.
+};
+
+#define MIN_RESOLVER_THREADS 2
+#define MAX_RESOLVER_THREADS 10
+
+static SDLNet_Address *resolver_queue = NULL;
+static SDL_Thread *resolver_threads[MAX_RESOLVER_THREADS];
+static SDL_Mutex *resolver_lock = NULL;
+static SDL_Condition *resolver_condition = NULL;
+static SDL_AtomicInt resolver_shutdown;
+static SDL_AtomicInt resolver_num_threads;
+static SDL_AtomicInt resolver_num_requests;
+static SDL_AtomicInt resolver_percent_loss;
+
+// I really want an SDL_random().  :(
+static int random_seed = 0;
+static int RandomNumber(void)
+{
+    // this is POSIX.1-2001's potentially bad suggestion, but we're not exactly doing cryptography here.
+    random_seed = random_seed * 1103515245 + 12345;
+    return (int) ((unsigned int) (random_seed / 65536) % 32768);
+}
+
+// between lo and hi (inclusive; it can return lo or hi itself, too!).
+static int RandomNumberBetween(const int lo, const int hi)
+{
+    return (RandomNumber() % (hi + 1 - lo)) + lo;
+}
+
+// !!! FIXME: replace strerror.
+// !!! FIXME: replace errno.
+static int LastSocketError(void)
+{
+#ifdef __WINDOWS__
+    return WSAGetLastError();
+#else
+    return errno;
+#endif
+}
+
+static int CloseSocketHandle(Socket handle)
+{
+#ifdef __WINDOWS__
+    return closesocket(handle);
+#else
+    return close(handle);
+#endif
+}
+
+// this blocks!
+static int ResolveAddress(SDLNet_Address *addr)
+{
+    SDL_assert(addr != NULL);  // we control all this, so this shouldn't happen.
+    struct addrinfo *ainfo = NULL;
+    int rc;
+
+//SDL_Log("getaddrinfo '%s'", addr->hostname);
+    rc = getaddrinfo(addr->hostname, NULL, NULL, &ainfo);
+//SDL_Log("rc=%d", rc);
+    if (rc != 0) {
+        addr->errstr = SDL_strdup((rc == EAI_SYSTEM) ? strerror(errno) : gai_strerror(rc));
+        return -1;  // error
+    } else if (ainfo == NULL) {
+        addr->errstr = SDL_strdup("Unknown error (query succeeded but result was NULL!)");
+        return -1;
+    }
+
+    char buf[128];
+    rc = getnameinfo(ainfo->ai_addr, ainfo->ai_addrlen, buf, sizeof (buf), NULL, 0, NI_NUMERICHOST);
+    if (rc != 0) {
+        addr->errstr = SDL_strdup((rc == EAI_SYSTEM) ? strerror(errno) : gai_strerror(rc));
+        freeaddrinfo(ainfo);
+        return -1;  // error
+    }
+
+    addr->human_readable = SDL_strdup(buf);
+    addr->ainfo = ainfo;
+    return 1;  // success (zero means "still in progress").
+}
+
+static int SDLCALL ResolverThread(void *data)
+{
+    const int threadnum = (int) ((size_t) data);
+    //SDL_Log("ResolverThread #%d starting up!", threadnum);
+
+    SDL_LockMutex(resolver_lock);
+
+    while (!SDL_AtomicGet(&resolver_shutdown)) {
+        SDLNet_Address *addr = SDL_AtomicGetPtr((void **) &resolver_queue);
+        if (!addr) {
+            if (SDL_AtomicGet(&resolver_num_threads) > MIN_RESOLVER_THREADS) {  // nothing pending and too many threads waiting in reserve? Quit.
+                SDL_DetachThread(resolver_threads[threadnum]);  // detach ourselves so no one has to wait on us.
+                SDL_AtomicSetPtr((void **) &resolver_threads[threadnum], NULL);
+                break;  // we quit. They'll spawn new threads if necessary.
+            }
+
+            // Block until there's something to do.
+            SDL_WaitCondition(resolver_condition, resolver_lock);  // surrenders the lock, sleeps until alerted, then relocks.
+            continue;  // check for shutdown and new work again!
+        }
+
+        SDL_AtomicSetPtr((void **) &resolver_queue, addr->resolver_next);   // take this task off the list, then release the lock so others can work.
+        SDL_UnlockMutex(resolver_lock);
+
+        //SDL_Log("ResolverThread #%d got new task ('%s')", threadnum, addr->hostname);
+
+        const int simulated_loss = SDL_AtomicGet(&resolver_percent_loss);
+
+        if (simulated_loss && (RandomNumberBetween(0, 100) > simulated_loss)) {
+            // won the percent_loss lottery? Delay resolving this address between 250 and 7000 milliseconds
+            SDL_Delay(RandomNumberBetween(250, 2000 + (50 * simulated_loss)));
+        }
+
+        int outcome;
+        if (!simulated_loss || (RandomNumberBetween(0, 100) > simulated_loss)) {
+            outcome = ResolveAddress(addr);            
+        } else {
+            outcome = -1;
+            addr->errstr = SDL_strdup("simulated failure");
+        }
+
+        SDL_AtomicSet(&addr->status, outcome);
+        //SDL_Log("ResolverThread #%d finished current task (%s, '%s' => '%s')", threadnum, (outcome < 0) ? "failure" : "success", addr->hostname, (outcome < 0) ? addr->errstr : addr->human_readable);
+
+        SDLNet_UnrefAddress(addr);  // we're done with it, but others might still own it.
+
+        SDL_AtomicAdd(&resolver_num_requests, -1);
+
+        // okay, we're done with this task, grab the lock so we can see what's next.
+        SDL_LockMutex(resolver_lock);
+        SDL_BroadcastCondition(resolver_condition);  // wake up anything waiting on results, and also give all resolver threads a chance to see if they are still needed.
+    }
+
+    SDL_AtomicAdd(&resolver_num_threads, -1);
+    SDL_UnlockMutex(resolver_lock);  // we're quitting, let go of the lock.
+
+    //SDL_Log("ResolverThread #%d ending!", threadnum);
+    return 0;
+}
+
+static SDL_Thread *SpinResolverThread(const int num)
+{
+    char name[16];
+    SDL_snprintf(name, sizeof (name), "SDLNetRslv%d", num);
+    SDL_assert(resolver_threads[num] == NULL);
+    SDL_AtomicAdd(&resolver_num_threads, 1);
+    resolver_threads[num] = SDL_CreateThreadWithStackSize(ResolverThread, name, 64 * 1024, (void *) ((size_t) num));
+    if (!resolver_threads[num]) {
+        SDL_AtomicAdd(&resolver_num_threads, -1);
+    }
+    return resolver_threads[num];
+}
+
+static void DestroyAddress(SDLNet_Address *addr)
+{
+    if (addr) {
+        if (addr->ainfo) {
+            freeaddrinfo(addr->ainfo);
+        }
+        SDL_free(addr->hostname);
+        SDL_free(addr->human_readable);
+        SDL_free(addr->errstr);
+        SDL_free(addr);
+    }
+}
+
+int SDLNet_Init(void)
+{
+    #ifdef __WINDOWS__
+    WSADATA data;
+    if (WSAStartup(MAKEWORD(1, 1), &data) != 0) {
+        return SDL_SetError("WSAStartup() failed");
+    }
+    #else
+    signal(SIGPIPE, SIG_IGN);
+    #endif
+
+    SDL_zeroa(resolver_threads);
+    SDL_AtomicSet(&resolver_shutdown, 0);
+    SDL_AtomicSet(&resolver_num_threads, 0);
+    SDL_AtomicSet(&resolver_num_requests, 0);
+    SDL_AtomicSet(&resolver_percent_loss, 0);
+    resolver_queue = NULL;
+
+    resolver_lock = SDL_CreateMutex();
+    if (!resolver_lock) {
+        goto failed;
+    }
+
+    resolver_condition = SDL_CreateCondition();
+    if (!resolver_condition) {
+        goto failed;
+    }
+
+    for (int i = 0; i < MIN_RESOLVER_THREADS; i++) {
+        if (!SpinResolverThread(i)) {
+            goto failed;
+        }
+    }
+
+    random_seed = (int) ((unsigned int) (SDL_GetPerformanceCounter() & 0xFFFFFFFF));
+
+    return 0;  // good to go.
+
+failed:
+    char *origerrstr = SDL_strdup(SDL_GetError());
+
+    SDLNet_Quit();
+
+    if (origerrstr) {
+        SDL_SetError("%s", origerrstr);
+        SDL_free(origerrstr);
+    }
+
+    return -1;
+}
+
+void SDLNet_Quit(void)
+{
+    if (resolver_lock && resolver_condition) {
+        SDL_LockMutex(resolver_lock);
+        SDL_AtomicSet(&resolver_shutdown, 1);
+        for (int i = 0; i < SDL_arraysize(resolver_threads); i++) {
+            if (resolver_threads[i]) {
+                SDL_BroadcastCondition(resolver_condition);
+                SDL_UnlockMutex(resolver_lock);
+                SDL_WaitThread(resolver_threads[i], NULL);
+                SDL_LockMutex(resolver_lock);
+                resolver_threads[i] = NULL;
+            }
+        }
+        SDL_UnlockMutex(resolver_lock);
+    }
+
+    SDL_AtomicSet(&resolver_shutdown, 0);
+    SDL_AtomicSet(&resolver_num_threads, 0);
+    SDL_AtomicSet(&resolver_num_requests, 0);
+    SDL_AtomicSet(&resolver_percent_loss, 0);
+
+    if (resolver_condition) {
+        SDL_DestroyCondition(resolver_condition);
+        resolver_condition = NULL;
+    }
+
+    if (resolver_lock) {
+        SDL_DestroyMutex(resolver_lock);
+        resolver_lock = NULL;
+    }
+
+    resolver_queue = NULL;
+
+    #ifdef __WINDOWS__
+    WSACleanup();
+    #endif
+}
+
+SDLNet_Address *SDLNet_ResolveHostname(const char *host)
+{
+    SDLNet_Address *addr = SDL_calloc(1, sizeof (SDLNet_Address));
+    if (!addr) {
+        SDL_OutOfMemory();
+        return NULL;
+    }
+
+    addr->hostname = SDL_strdup(host);
+    if (!addr->hostname) {
+        SDL_free(addr);
+        SDL_OutOfMemory();
+        return NULL;
+    }
+
+    SDL_AtomicSet(&addr->refcount, 2);  // one for creation, one for the resolver thread to unref when done.
+
+    SDL_LockMutex(resolver_lock);
+
+    // !!! FIXME: this should append to the list, not prepend; as is, new requests will make existing pending requests take longer to start processing.
+    SDL_AtomicSetPtr((void **) &addr->resolver_next, SDL_AtomicGetPtr((void **) &resolver_queue));
+    SDL_AtomicSetPtr((void **) &resolver_queue, addr);
+
+    const int num_threads = SDL_AtomicGet(&resolver_num_threads);
+    const int num_requests = SDL_AtomicAdd(&resolver_num_requests, 1) + 1;
+    //SDL_Log("num_threads=%d, num_requests=%d", num_threads, num_requests);
+    if ((num_requests >= num_threads) && (num_threads < MAX_RESOLVER_THREADS)) {  // all threads are busy? Maybe spawn a new one.
+        // if this didn't actually spin one up, it is what it is...the existing threads will eventually get there.
+        for (int i = 0; i < SDL_arraysize(resolver_threads); i++) {
+            if (!resolver_threads[i]) {
+                SpinResolverThread(i);
+                break;
+            }
+        }
+    }
+
+    SDL_SignalCondition(resolver_condition);
+    SDL_UnlockMutex(resolver_lock);
+
+    return addr;
+}
+
+int SDLNet_WaitForResolution(SDLNet_Address *addr)
+{
+    if (!addr) {
+        return SDL_InvalidParamError("address");  // obviously nothing to wait for.
+    }
+
+    // we _could_ use a different lock for this, but this is Good Enough.
+    SDL_LockMutex(resolver_lock);
+    while (SDL_AtomicGet(&addr->status) == 0) {
+        SDL_WaitCondition(resolver_condition, resolver_lock);
+    }
+    SDL_UnlockMutex(resolver_lock);
+
+    return SDL_AtomicGet(&addr->status);
+}
+
+int SDLNet_GetAddressStatus(SDLNet_Address *addr)
+{
+    if (!addr) {
+        return SDL_InvalidParamError("address");
+    }
+    const int retval = SDL_AtomicGet(&addr->status);
+    if (retval == -1) {
+        SDL_SetError("%s", (const char *) SDL_AtomicGetPtr((void **) &addr->errstr));
+    }
+    return retval;
+}
+
+const char *SDLNet_GetAddressString(SDLNet_Address *addr)
+{
+    return addr ? (const char *) SDL_AtomicGetPtr((void **) &addr->human_readable) : NULL;
+}
+
+void SDLNet_RefAddress(SDLNet_Address *addr)
+{
+    if (addr) {
+        SDL_AtomicIncRef(&addr->refcount);
+    }
+}
+
+void SDLNet_UnrefAddress(SDLNet_Address *addr)
+{
+    if (addr && SDL_AtomicDecRef(&addr->refcount)) {
+        DestroyAddress(addr);
+    }
+}
+
+void SDLNet_SimulateAddressResolutionLoss(int percent_loss)
+{
+    percent_loss = SDL_min(100, percent_loss);
+    percent_loss = SDL_max(0, percent_loss);
+    SDL_AtomicSet(&resolver_percent_loss, percent_loss);
+}
+
+SDLNet_Address **SDLNet_GetLocalAddresses(int *num_addresses)
+{
+    // !!! FIXME: write me!
+    return NULL;
+}
+
+void SDLNet_FreeLocalAddresses(SDLNet_Address **addresses)
+{
+    if (addresses) {
+        for (int i = 0; addresses[i] != NULL; i++) {
+            SDLNet_UnrefAddress(addresses[i]);
+        }
+        SDL_free(addresses);
+    }
+}
+
+static struct addrinfo *MakeAddrInfoWithPort(const SDLNet_Address *addr, const int socktype, const Uint16 port)
+{
+    const struct addrinfo *ainfo = addr ? addr->ainfo : NULL;
+    SDL_assert(!addr || ainfo);
+
+    // we need to set up a sockaddr with the port in it for connect(), etc, which is kind of a pain, since we
+    // want to keep things generic and also not set up a port at resolve time.
+    struct addrinfo hints;
+    SDL_zero(hints);
+    hints.ai_family = ainfo ? ainfo->ai_family : AF_UNSPEC;
+    hints.ai_socktype = socktype;
+    hints.ai_protocol = ainfo ? ainfo->ai_protocol : 0;
+    hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV | (!ainfo ? AI_PASSIVE : 0);
+
+    char service[16];
+    SDL_snprintf(service, sizeof (service), "%d", (int) port);
+
+    struct addrinfo *addrwithport = NULL;
+    int rc = getaddrinfo(addr ? addr->human_readable : NULL, service, &hints, &addrwithport);
+    if (rc != 0) {
+        SDL_SetError("Failed to prepare address with port: %s", (rc == EAI_SYSTEM) ? strerror(errno) : gai_strerror(rc));
+        return NULL;
+    }
+
+    return addrwithport;
+}
+
+
+struct SDLNet_StreamSocket
+{
+    SDLNet_Address *addr;
+    Uint16 port;
+    Socket handle;
+    int status;
+    Uint8 *pending_output_buffer;
+    int pending_output_len;
+    int pending_output_allocation;
+    int percent_loss;
+    Uint64 simulated_failure_until;
+};
+
+static int MakeSocketNonblocking(Socket handle)
+{
+    #ifdef __WINDOWS__
+    DWORD one = 1;
+    return ioctlsocket(handle, FIONBIO, &one);
+    #else
+    return fcntl(handle, F_SETFL, fcntl(handle, F_GETFL, 0) | O_NONBLOCK);
+    #endif
+}
+
+static SDL_bool WouldBlock(const int err)
+{
+    #ifdef __WINDOWS__
+    return (err == WSAEWOULDBLOCK) ? SDL_TRUE : SDL_FALSE;
+    #else
+    return ((err == EWOULDBLOCK) || (err == EAGAIN) || (err == EINPROGRESS)) ? SDL_TRUE : SDL_FALSE;
+    #endif
+}
+
+SDLNet_StreamSocket *SDLNet_CreateClient(SDLNet_Address *addr, Uint16 port)
+{
+    if (addr == NULL) {
+        SDL_InvalidParamError("address");
+        return NULL;
+    } else if (SDL_AtomicGet(&addr->status) != 1) {
+        SDL_SetError("Address is not resolved");
+        return NULL;
+    }
+
+    SDLNet_StreamSocket *sock = (SDLNet_StreamSocket *) SDL_calloc(1, sizeof (SDLNet_StreamSocket));
+    if (!sock) {
+        SDL_OutOfMemory();
+        return NULL;
+    }
+
+    sock->addr = addr;
+    sock->port = port;
+
+    // we need to set up a sockaddr with the port in it for connect(), which is kind of a pain, since we
+    // want to keep things generic and also not set up a port at resolve time.
+    struct addrinfo *addrwithport = MakeAddrInfoWithPort(addr, SOCK_STREAM, port);
+    if (!addrwithport) {
+        SDL_free(sock);
+        return NULL;
+    }
+
+    sock->handle = socket(addrwithport->ai_family, addrwithport->ai_socktype, addrwithport->ai_protocol);
+    if (sock->handle == INVALID_SOCKET) {
+        SDL_SetError("Failed to create socket: %s", strerror(errno));
+        freeaddrinfo(addrwithport);
+        SDL_free(sock);
+        return NULL;
+    }
+
+    if (MakeSocketNonblocking(sock->handle) < 0) {
+        CloseSocketHandle(sock->handle);
+        freeaddrinfo(addrwithport);
+        SDL_free(sock);
+        SDL_SetError("Failed to make new socket non-blocking");
+        return NULL;
+    }
+
+    const int rc = connect(sock->handle, addrwithport->ai_addr, addrwithport->ai_addrlen);
+
+    freeaddrinfo(addrwithport);
+
+    if (rc == SOCKET_ERROR) {
+        const int err = LastSocketError();
+        if (!WouldBlock(err)) {
+            SDL_SetError("Connection failed at startup: %s", strerror(err));
+            CloseSocketHandle(sock->handle);
+            SDL_free(sock);
+            return NULL;
+        }
+    }
+
+    SDLNet_RefAddress(addr);
+    return sock;
+}
+
+static int CheckClientConnection(SDLNet_StreamSocket *sock, int timeoutms)
+{
+    if (!sock) {
+        return SDL_InvalidParamError("sock");
+    } else if (sock->status == 0) {  // still pending?
+        /*!!! FIXME: add this later? if (sock->simulated_failure_ticks) {
+            if (SDL_GetTicks() >= sock->simulated_failure_ticks) {
+                sock->status = SDL_SetError("simulated failure");
+            }
+        } else*/ {
+            struct pollfd pfd;
+            SDL_zero(pfd);
+            pfd.fd = sock->handle;
+            pfd.events = POLLOUT;
+            if (poll(&pfd, 1, timeoutms) == SOCKET_ERROR) {
+                sock->status = SDL_SetError("Failed to poll socket: %s", strerror(errno));
+            } else if ((pfd.revents & (POLLERR|POLLHUP|POLLNVAL)) != 0) {
+                int err = 0;
+                SockLen errsize = sizeof (err);
+                getsockopt(sock->handle, SOL_SOCKET, SO_ERROR, &err, &errsize);
+                sock->status = SDL_SetError("Socket failed to connect: %s", strerror(err));
+            } else if (pfd.revents & POLLOUT) {
+                sock->status = 1;  // good to go!
+            }
+        }
+    }
+    return sock->status;
+}
+
+int SDLNet_WaitForConnection(SDLNet_StreamSocket *sock)
+{
+    return CheckClientConnection(sock, -1);  // infinite wait
+}
+
+int SDLNet_GetConnectionStatus(SDLNet_StreamSocket *sock)
+{
+    return CheckClientConnection(sock, 0);
+}
+
+
+struct SDLNet_Server
+{
+    SDLNet_Address *addr;  // bound to this address (NULL for any).
+    Uint16 port;
+    Socket handle;
+};
+
+SDLNet_Server *SDLNet_CreateServer(SDLNet_Address *addr, Uint16 port)
+{
+    if (addr && SDL_AtomicGet(&addr->status) != 1) {
+        SDL_SetError("Address is not resolved");  // strictly speaking, this should be a local interface, but a resolved address can fail later.
+        return NULL;
+   

(Patch may be truncated, please check the link at the top of this post.)