diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b4ebd3f --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Shell scripts keep LF on every checkout, so bash in WSL can run a Windows checkout. +*.sh text eol=lf diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c639fb0..02145ff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -59,17 +59,81 @@ jobs: files: coverage.xml disable_search: true + # Native Windows with MSVC, built with GNU make from Git Bash. Runs st_utest, then + # auto/tools.sh in the ST default and SRS builds, as on Linux and macOS. + windows: + name: actions-test-build-win64 + runs-on: windows-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + ################################################################ + # Build + - name: Setup MSVC environment + shell: pwsh + run: | + # Pass the vcvars64.bat environment to the next steps: INCLUDE, LIB, LIBPATH, + # and only the PATH directories that vcvars64.bat adds. + $vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vs = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $vs) { throw "No Visual Studio with the MSVC x64 tools" } + $vars = cmd /c "`"$vs\VC\Auxiliary\Build\vcvars64.bat`" >nul && set" + if ($LASTEXITCODE -ne 0) { throw "vcvars64.bat failed" } + $old = $env:PATH -split ';' + foreach ($line in $vars) { + if ($line -match '^(INCLUDE|LIB|LIBPATH)=(.*)$') { + [IO.File]::AppendAllText($env:GITHUB_ENV, "$($Matches[1])=$($Matches[2])`n") + } elseif ($line -match '^PATH=(.*)$') { + foreach ($dir in $Matches[1] -split ';') { + if ($dir -and $old -notcontains $dir) { [IO.File]::AppendAllText($env:GITHUB_PATH, "$dir`n") } + } + } + } + - name: Build ST and utest with MSVC + shell: bash + run: | + command -v make >/dev/null || choco install make --no-progress -y + command -v make cl ml64 lib + make win64-debug-utest + test -f obj/st_utest.exe + + ################################################################ + # Tests + - name: Run ST utest + shell: bash + timeout-minutes: 10 + run: ./obj/st_utest.exe + - name: Run ST tools + shell: bash + timeout-minutes: 10 + run: ./auto/tools.sh + - name: Run ST tools in the SRS build + shell: bash + timeout-minutes: 10 + run: EXTRA_CFLAGS=-DMALLOC_STACK ./auto/tools.sh + + ################################################################ + # Build + - name: Build optimized ST with MSVC + shell: bash + run: | + make win64-optimized + test -f obj/libst.a + # The single check to require for merging. It runs even when a needed job # fails, because a skipped required check counts as passed. final: name: actions-test-final runs-on: ubuntu-24.04 - needs: [utest, coverage] + needs: [utest, coverage, windows] if: always() steps: - name: Check all jobs succeeded run: | - echo "utest: ${{ needs.utest.result }}, coverage: ${{ needs.coverage.result }}" + echo "utest: ${{ needs.utest.result }}, coverage: ${{ needs.coverage.result }}, windows: ${{ needs.windows.result }}" test "${{ needs.utest.result }}" = success test "${{ needs.coverage.result }}" = success + test "${{ needs.windows.result }}" = success diff --git a/.gitignore b/.gitignore index ab54028..ee41297 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,12 @@ DARWIN_*_DBG LINUX_*_DBG CYGWIN64_*_DBG +WIN64_*_DBG +WIN64_*_OPT +tools/*/*.exe +tools/*/*.obj +tools/*/*.ilk +tools/*/*.pdb obj st.pc .idea diff --git a/CHANGELOG.md b/CHANGELOG.md index 66e9526..6b8402e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ The changelog for the [srs](https://github.com/ossrs/state-threads/tree/srs) bra ## ST 1.9 Changelog +* 2026-10-03, Windows: Support native Windows x64 with MSVC, MASM, and WSAPoll, built by GNU make. v1.9.2 * 2026-10-03, Tools: Add integration tools for every public API, run by auto/tools.sh in CI, [#41](https://github.com/ossrs/state-threads/pull/41). v1.9.1 * 2026-09-30, Changelog: Move the change list from README to CHANGELOG.md, and add the patch version. v1.9.0 diff --git a/Makefile b/Makefile index 7832b36..6f81dc2 100644 --- a/Makefile +++ b/Makefile @@ -33,13 +33,14 @@ # GPL. # This is the full version of the libst library - modify carefully -VERSION = 1.9.1 +VERSION = 1.9.2 ########################## # Supported OSes: # #OS = DARWIN #OS = LINUX +#OS = WIN64 # Please see the "Other possible defines" section below for # possible compilation options. @@ -77,6 +78,13 @@ ARFLAGS = -r LNFLAGS = -s DSO_SUFFIX = so +# The output flags of the compiler and archiver, the debug flags, and the +# command that points obj at TARGETDIR. WIN64 (MSVC) overrides them. +CC_OUT = -o $(EMPTY) +AR_OUT = +DBG_FLAGS = -g -O0 +LINK_OBJ = $(LN) $(LNFLAGS) $(TARGETDIR) obj + MAJOR = $(shell echo $(VERSION) | sed 's/^\([^\.]*\).*/\1/') DESC = st.pc @@ -86,11 +94,13 @@ DESC = st.pc TARGETS = darwin-debug darwin-optimized \ linux-debug linux-optimized \ - cygwin64-debug + cygwin64-debug \ + win64-debug win64-optimized UTEST_TARGETS = darwin-debug-utest linux-debug-utest \ darwin-debug-gcov linux-debug-gcov \ - cygwin64-debug-utest + cygwin64-debug-utest \ + win64-debug-utest # # Platform specifics @@ -127,6 +137,23 @@ OTHER_FLAGS = -Wall DEFINES += -DMD_HAVE_SELECT endif +# Native Windows x64 with MSVC (cl, lib), not CYGWIN64. Run GNU make from Git +# Bash or PowerShell with the MSVC environment on PATH. Static library only. +# Git Bash 'ln -s' copies instead of linking, so obj is a directory junction. +ifeq ($(OS), WIN64) +EXTRA_OBJS = $(TARGETDIR)/md_win64.o +CC = cl +CXX = cl +AR = lib +RANLIB = true +ARFLAGS = -nologo +CC_OUT = -Fo +AR_OUT = -OUT: +DBG_FLAGS = -Z7 -Od +LINK_OBJ = MSYS2_ARG_CONV_EXCL='*' cmd /c mklink /J obj $(TARGETDIR) >/dev/null +OTHER_FLAGS = -nologo -W4 +endif + # # End of platform section. ########################## @@ -135,7 +162,7 @@ endif ifeq ($(BUILD), OPT) OTHER_FLAGS += -O2 else -OTHER_FLAGS += -g -O0 +OTHER_FLAGS += $(DBG_FLAGS) DEFINES += -DDEBUG endif @@ -199,6 +226,9 @@ endif # or enable support for asan: # make EXTRA_CFLAGS="-DMD_ASAN -fsanitize=address -fno-omit-frame-pointer" # +# or enable support for asan on native Windows, where cl has no -fno-omit-frame-pointer: +# make win64-debug EXTRA_CFLAGS="-DMD_ASAN -fsanitize=address" +# # or to disable the clock_gettime for MacOS before 10.12, see https://github.com/ossrs/srs/issues/3978 # make EXTRA_CFLAGS=-DMD_OSX_NO_CLOCK_GETTIME # @@ -261,13 +291,13 @@ st.pc: st.pc.in # Point obj to this platform on every build, even when the library is up to date. .PHONY: obj-link obj-link: - rm -f obj; $(LN) $(LNFLAGS) $(TARGETDIR) obj + rm -f obj; $(LINK_OBJ) $(TARGETDIR): if [ ! -d $(TARGETDIR) ]; then mkdir $(TARGETDIR); fi $(SLIBRARY): $(OBJS) - $(AR) $(ARFLAGS) $@ $(OBJS) + $(AR) $(ARFLAGS) $(AR_OUT)$@ $(OBJS) $(RANLIB) $@ $(DLIBRARY): $(OBJS:%.o=%-pic.o) @@ -286,8 +316,14 @@ $(HEADER): public.h $(TARGETDIR)/%.o: %.S $(CC) $(CFLAGS) -c $< -o $@ +# The WIN64 context switch is MASM assembly, md_win64.asm. +ifeq ($(OS), WIN64) +$(TARGETDIR)/%.o: %.asm + ml64 -nologo -c -Fo$@ $< +endif + $(TARGETDIR)/%.o: %.c common.h md.h - $(CC) $(CFLAGS) -c $< -o $@ + $(CC) $(CFLAGS) -c $< $(CC_OUT)$@ # Note that we use C++98 standard for the C++ files. $(TARGETDIR)/%.o: %.cc common.h md.h @@ -325,6 +361,11 @@ linux-optimized: cygwin64-debug: $(MAKE) OS="CYGWIN64" BUILD="DBG" +win64-debug: + $(MAKE) OS="WIN64" BUILD="DBG" +win64-optimized: + $(MAKE) OS="WIN64" BUILD="OPT" + darwin-debug-utest: @echo "Build utest for state-threads" $(MAKE) OS="DARWIN" BUILD="DBG" @@ -337,6 +378,10 @@ cygwin64-debug-utest: @echo "Build utest for state-threads" $(MAKE) OS="CYGWIN64" BUILD="DBG" cd utest && $(MAKE) UTEST_FLAGS="-std=gnu++0x" # @see https://www.codenong.com/18784112/ +win64-debug-utest: + @echo "Build utest for state-threads" + $(MAKE) OS="WIN64" BUILD="DBG" + cd utest && $(MAKE) OS="WIN64" darwin-debug-gcov: @echo "Build utest with gcov for state-threads" diff --git a/README.md b/README.md index dd25054..38a4892 100644 --- a/README.md +++ b/README.md @@ -86,13 +86,42 @@ Get code: git clone -b srs https://github.com/ossrs/state-threads.git ``` -For Cygwin(Windows): +Native Windows x64 builds with the Visual Studio (MSVC) toolchain, `cl`, `ml64`, and `lib`, through GNU make +and the same Makefile. There is no Cygwin, MinGW, WSL, or POSIX emulation layer: GNU make and Git Bash only +run the build, and the output is the static library `obj/libst.a` built by MSVC. It needs Windows 10 1809 or +Windows Server 2019 or later, x64. + +Install the build tools: + +* Visual Studio 2022, or its Build Tools, with the MSVC x64 tools (the "Desktop development with C++" workload). +* [Git for Windows](https://git-scm.com/download/win), for Git Bash. +* GNU make, for example `winget install ezwinports.make` or `choco install make`. + +Open the `x64 Native Tools Command Prompt for VS 2022`, which runs `vcvars64.bat` and puts `cl`, `ml64`, and +`lib` on `PATH`, then start Git Bash in it: ``` -make cygwin64-debug +"C:\Program Files\Git\bin\bash.exe" ``` -> Remark: Windows native build is unsupported right now. +For Windows, build the debug or the optimized library: + +```bash +make win64-debug +make win64-optimized +``` + +> Note: Programs that include `st.h` must include it before `windows.h`, or define `WIN32_LEAN_AND_MEAN`. The +> library links `ws2_32.lib` by itself. For what works differently on Windows, read the +> [porting notes](docs/notes.html#windows). + +Windows with ASAN(Google Address Sanitizer), where cl has no `-fno-omit-frame-pointer`: + +```bash +make win64-debug EXTRA_CFLAGS="-DMD_ASAN -fsanitize=address" +``` + +> Remark: Cygwin64 is deprecated, do not use it. Use native Windows, as above. ## Branch SRS @@ -148,6 +177,57 @@ To make ST with utest and run it: make darwin-debug-utest && ./obj/st_utest ``` +## Windows: UTest + +> Note: We use [Google test](https://github.com/google/googletest/releases/tag/release-1.11.0) in `utest/gtest-fit`. + +To make ST with utest and run it, in Git Bash with the MSVC environment (see [Windows: Usage](#windows-usage)): + +```bash +make win64-debug-utest && ./obj/st_utest.exe +``` + +The tests that need POSIX features (fork, signals, pipes, files) are not built on Windows, and a few are +skipped with a reason, for example the write tests that need a partial send, which Winsock never does. + +To debug in Visual Studio, generate a solution with CMake, which comes with Visual Studio, from the +`x64 Native Tools Command Prompt for VS 2022`, then open `C:\st-build\st.sln`: + +``` +cmake -S cmake -B C:\st-build -G "Visual Studio 17 2022" -A x64 +cmake --build C:\st-build --config Debug --target st_utest +``` + +> Note: Use a short build directory, because MSBuild fails when its paths are too long. The Makefile stays the +> main build, and CMake also builds the `helloworld`, `porting`, and `verify` tools. + +## Windows: Tools + +The integration tools in `tools/` build with MSVC and run with the same runner as on Linux and macOS, in Git +Bash with the MSVC environment (see [Windows: Usage](#windows-usage)). It builds the library with +`make -B win64-debug`, then builds each tool and runs `.exe` with `ST_TOOL_EVENTSYS=select` and `alt` +(both are WSAPoll on Windows), and prints `All tools OK`. Run it in the ST default build and in the SRS +build, as CI does: + +```bash +./auto/tools.sh +EXTRA_CFLAGS=-DMALLOC_STACK ./auto/tools.sh +``` + +The arguments name the tools to run, for example `./auto/tools.sh tcp udp`. The runner leaves the library +built with `EXTRA_CFLAGS`, so rebuild with `make -B win64-debug-utest` before the utest. + +On Windows, ST is sockets only, so the tools make each pipe a loopback TCP pair and each `socketpair` an +`AF_UNIX` stream pair, or a loopback UDP pair for datagrams. The `pipe` tool, which needs pipes, FIFOs, +`st_open` files, and signals, prints `SKIP pipe: ` instead of `OK`. A few checks that Windows does not +support are skipped there, each with a comment: the partial writes on timeout in `tcp` (Winsock never sends +part of a non-blocking send), the `RLIMIT_NOFILE` checks in `lifecycle` and `stress`, reading back +`O_NONBLOCK` in `poll`, and the `AF_UNIX` datagram echo and the `ENOENT` of a missing path in `unix`. + +The `exception` tool is C++: it throws and catches C++ exceptions on coroutine stacks, and on Windows also +raises and catches SEH exceptions there. Windows builds C++ exceptions on SEH, which rejects frames outside +the stack bounds in the TIB, so this works only because ST switches those bounds with each coroutine stack. + ## Linux: Coverage > Note: We use [Google test](https://github.com/google/googletest/releases/tag/release-1.11.0) in `utest/gtest-fit`. diff --git a/auto/tools.sh b/auto/tools.sh index f2a644e..af3289a 100755 --- a/auto/tools.sh +++ b/auto/tools.sh @@ -8,18 +8,39 @@ # # Examples: # ./auto/tools.sh +# ./auto/tools.sh tcp udp # EXTRA_CFLAGS=-DMALLOC_STACK ./auto/tools.sh # EXTRA_CFLAGS="-DMALLOC_STACK -DMD_ASAN -fsanitize=address -fno-omit-frame-pointer" \ # LDFLAGS=-fsanitize=address ./auto/tools.sh # +# The arguments name the tools to run, every folder in tools/ by default. # Each tool reads ST_TOOL_EVENTSYS, select or alt. It prints " OK" # per run, or "FAILED " with the output, and exits 1 at the first failure. +# A tool that this platform does not support prints only "SKIP : " and exits 0, +# which shows as "SKIP : ()". +# +# On native Windows, run it from Git Bash with the MSVC environment on PATH, where +# uname -s is MINGW64_NT-* or MSYS_NT-*: it builds win64-debug and runs .exe. cd "$(dirname "$0")/.." || exit 1 ST_TARGET=linux-debug -if [[ $(uname -s) == Darwin ]]; then - ST_TARGET=darwin-debug +EXE= +case $(uname -s) in + Darwin) ST_TARGET=darwin-debug ;; + MINGW*|MSYS*) ST_TARGET=win64-debug; EXE=.exe ;; +esac + +tool_dirs=() +for name in "$@"; do + if [[ ! -f tools/$name/Makefile ]]; then + echo "FAILED no tool $name in tools/" + exit 1 + fi + tool_dirs+=("tools/$name/") +done +if [[ ${#tool_dirs[@]} == 0 ]]; then + tool_dirs=(tools/*/) fi echo "Build ST with make -B $ST_TARGET EXTRA_CFLAGS=\"$EXTRA_CFLAGS\"" @@ -30,21 +51,30 @@ if ! out=$(make -B $ST_TARGET EXTRA_CFLAGS="$EXTRA_CFLAGS" 2>&1); then exit 1 fi -for dir in tools/*/; do +for dir in "${tool_dirs[@]}"; do name=$(basename "$dir") # -W relinks the tool, because the binaries are shared across platforms and builds. - if ! out=$(make -C "$dir" -W "$name.c" LDFLAGS="$LDFLAGS" 2>&1); then + # A tool is C, .c, or C++, .cpp. + src=$name.c + if [[ -f $dir$name.cpp ]]; then + src=$name.cpp + fi + if ! out=$(make -C "$dir" -W "$src" LDFLAGS="$LDFLAGS" 2>&1); then echo "$out" echo "FAILED build $name" exit 1 fi for eventsys in select alt; do - if ! out=$(cd "$dir" && ST_TOOL_EVENTSYS=$eventsys "./$name" 2>&1); then + if ! out=$(cd "$dir" && ST_TOOL_EVENTSYS=$eventsys "./$name$EXE" 2>&1); then echo "$out" echo "FAILED $name $eventsys" exit 1 fi + if [[ $out == "SKIP $name:"* ]]; then + echo "$out ($eventsys)" + continue + fi echo "$name $eventsys OK" done done diff --git a/cmake/CMakeLists.txt b/cmake/CMakeLists.txt index a575a1e..61188fe 100644 --- a/cmake/CMakeLists.txt +++ b/cmake/CMakeLists.txt @@ -5,6 +5,10 @@ cmake_minimum_required(VERSION 3.10) # Language "C" is required for find_package(Threads). if (CMAKE_VERSION VERSION_LESS 3.0) project(st CXX C ASM) +elseif (CMAKE_HOST_WIN32) + # Native Windows uses MASM (ASM_MASM) below, not the GNU assembler (ASM). + cmake_policy(SET CMP0048 NEW) + project(st VERSION 4.0.0 LANGUAGES CXX C) else() cmake_policy(SET CMP0048 NEW) project(st VERSION 4.0.0 LANGUAGES CXX C ASM) @@ -13,6 +17,58 @@ endif() # For utest required C++11. set (CMAKE_CXX_STANDARD 11) +########################################################### +# Native Windows x64 with MSVC, the WIN64 platform of ../Makefile, which stays the main build. This gives a Visual +# Studio project of the utest for debugging, for example: +# cmake -S cmake -B build -G "Visual Studio 17 2022" -A x64 +# No bash, and the source tree is not written: st.h is copied to the build directory, so the obj junction made by +# make is left alone. +if (CMAKE_HOST_WIN32 AND NOT MSVC) + message(FATAL_ERROR "Only MSVC is supported on Windows, for example the Visual Studio generator") +endif() +if (MSVC) + get_filename_component(ST_DIR ${PROJECT_SOURCE_DIR}/.. ABSOLUTE) + message("ST home is ${ST_DIR}") + + # NOMINMAX keeps windows.h, included by winsock2.h, from breaking std::min and std::max. + ADD_DEFINITIONS("-DWIN64 -DDEBUG -DNOMINMAX") + + configure_file(${ST_DIR}/public.h ${PROJECT_BINARY_DIR}/st/st.h COPYONLY) + INCLUDE_DIRECTORIES(${PROJECT_BINARY_DIR}/st ${ST_DIR}/utest) + + foreach (FILE event.c io.c key.c sched.c stk.c sync.c common.c) + list(APPEND SOURCE_FILES ${ST_DIR}/${FILE}) + endforeach () + # The context switch is MASM assembly. + enable_language(ASM_MASM) + list(APPEND SOURCE_FILES ${ST_DIR}/md_win64.asm) + + # Google Test with the same DLL runtime (/MD) as the utest. + set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) + if (NOT TARGET gtest) + ADD_SUBDIRECTORY(${ST_DIR}/utest/gtest-fit gtest-fit) + endif () + INCLUDE_DIRECTORIES(${gtest_SOURCE_DIR}/include ${gtest_SOURCE_DIR}) + + set(ST_UTEST_SOURCE_FILES ${SOURCE_FILES}) + AUX_SOURCE_DIRECTORY(${ST_DIR}/utest ST_UTEST_SOURCE_FILES) + + ADD_EXECUTABLE(st_utest ${ST_UTEST_SOURCE_FILES}) + TARGET_LINK_LIBRARIES(st_utest gtest gtest_main ws2_32) + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT st_utest) + + # The tools ported to MSVC so far. + foreach (TOOL helloworld porting verify) + set(ST_TOOL_SOURCE_FILES ${SOURCE_FILES}) + AUX_SOURCE_DIRECTORY(${ST_DIR}/tools/${TOOL} ST_TOOL_SOURCE_FILES) + ADD_EXECUTABLE(st_${TOOL} ${ST_TOOL_SOURCE_FILES}) + TARGET_LINK_LIBRARIES(st_${TOOL} ws2_32) + endforeach () + + MESSAGE(STATUS "@see https://github.com/ossrs/state-threads#usage") + return() +endif() + ########################################################### execute_process( COMMAND bash -c "cd ${PROJECT_SOURCE_DIR}/../ && pwd" diff --git a/common.h b/common.h index f34079f..d1c6f9f 100644 --- a/common.h +++ b/common.h @@ -45,9 +45,13 @@ #define __ST_COMMON_H__ #include +#if !defined(WIN64) #include +#endif #include +#if !defined(WIN64) #include +#endif #include /* Enable assertions only if DEBUG is defined */ @@ -328,6 +332,21 @@ void _st_del_sleep_q(_st_thread_t *thread); _st_stack_t *_st_stack_new(int stack_size); void _st_stack_free(_st_stack_t *ts); int _st_io_init(void); +#if defined(WIN64) +int _st_win64_errno(int wsaerr, int connecting); +void _st_io_destroy(void); +/* The POSIX calls of io.c, with Winsock. */ +int _st_win64_ioctl(int fd, unsigned long request, int *arg); +int _st_win64_fcntl(int fd, int cmd, int arg); +int _st_win64_close(int fd); +ssize_t _st_win64_read(int fd, void *buf, size_t nbyte); +ssize_t _st_win64_write(int fd, const void *buf, size_t nbyte); +ssize_t _st_win64_readv(int fd, const struct iovec *iov, int iov_size); +ssize_t _st_win64_writev(int fd, const struct iovec *iov, int iov_size); +int _st_win64_recvfrom(int fd, void *buf, int len, int flags, struct sockaddr *from, socklen_t *fromlen); +int _st_win64_recvmsg(int fd, struct msghdr *msg, int flags); +int _st_win64_sendmsg(int fd, const struct msghdr *msg, int flags); +#endif st_utime_t st_utime(void); _st_cond_t *st_cond_new(void); diff --git a/docs/notes.html b/docs/notes.html index 5a24369..64549cb 100644 --- a/docs/notes.html +++ b/docs/notes.html @@ -102,6 +102,74 @@

Porting

  • NetBSD on x86, Alpha, SPARC, and VAX
  • MacOS X (Darwin) on PowerPC (32 bit) and Intel (both 32 and 64 bit) [universal]
  • Cygwin
  • +
  • Windows x64 (native, with MSVC; see Native Windows)
  • + +

    + + +

    Native Windows

    +The library builds on Windows 10 1809 or Windows Server 2019 and later, x64, +with the Visual Studio (MSVC) toolchain and no POSIX emulation layer: the +win64-debug, win64-optimized, and win64-debug-utest +Makefile targets run cl, ml64, and lib and build a +static library only. The platform is WIN64 (OS=WIN64 in the +Makefile), separate from CYGWIN64. The public API is unchanged, and +the platform-specific parts are: +

    +

      +
    • Thread context: md_win64.asm (MASM) saves every register +the Win64 ABI calls callee-saved: rbx, rbp, rdi, rsi, r12-r15, and xmm6-xmm15, +plus MXCSR and the x87 control word. It also switches the stack bounds in the +thread information block (TIB), without which C++ exceptions, crash dumps, and +debugger stack walks fail on a thread stack. MSVC long is 32 bits, so +the jmp_buf slots are long long. A new thread starts in the +assembly function _st_md_thread_start, which does not depend on what +the compiler keeps in registers. Do not link programs with +/CETCOMPAT: the context switch is not compatible with CET shadow +stacks.
    • +

      +

    • Stacks: VirtualAlloc, with PAGE_NOACCESS red zones +in DEBUG builds. With MALLOC_STACK there are no red +zones.
    • +

      +

    • Time: QueryPerformanceCounter.
    • +

      +

    • Event system: WSAPoll, for ST_EVENTSYS_DEFAULT, +ST_EVENTSYS_SELECT, and ST_EVENTSYS_ALT. It does not support +POLLPRI, so st_poll() fails with EINVAL for it, as +with kqueue. Timeouts have millisecond resolution and follow the system timer +tick (about 15.6 ms by default).
    • +

      +

    • I/O: sockets only, through Winsock. st_init() calls +WSAStartup(), and st_destroy() calls +WSACleanup(), once per OS thread. File descriptors stay int +in the API: a SOCKET has only 32 significant bits, so it can be cast +to int and back. Winsock errors are mapped to errno, for +example WSAEWOULDBLOCK to EAGAIN. Control data of +st_sendmsg() and st_recvmsg() uses the Winsock layout +(WSA_CMSG_*).
    • +
    +

    +Some things are not supported or behave differently on Windows: +

    +

      +
    • st_open() fails with ENOSYS: there is no polling of +files or FIFOs. There is no SIGPIPE to ignore, and no +RLIMIT_NOFILE: st_getfdlimit() returns 2^24, the per-process +handle limit.
    • +
    • A non-blocking send on a TCP socket is all or nothing: Winsock takes a whole +buffer while the send buffer is not full, even one larger than +SO_SNDBUF, and never sends part of it. So a large write may complete +at once, and the next write waits.
    • +
    • A connect to a loopback port with no listener fails with +ECONNREFUSED after about 2 seconds, because Windows retries the +connection; a shorter timeout gives ETIME.
    • +
    • On a UDP socket, an ICMP port unreachable makes the next receive fail with +ECONNREFUSED. A datagram larger than the buffer returns the bytes that +fit, as on POSIX.
    • +
    • MSVC AddressSanitizer works with MD_ASAN, but not with +-fsanitize-address-use-after-return, which gives false reports with +MSVC 14.44.

    diff --git a/docs/win64_coroutine.md b/docs/win64_coroutine.md new file mode 100644 index 0000000..ce0ad37 --- /dev/null +++ b/docs/win64_coroutine.md @@ -0,0 +1,186 @@ +# How coroutines work on native Windows x64 + +This note explains how State Threads switches and starts threads (coroutines) on native Windows x64 with +MSVC (`WIN64`), and why a new thread starts in an assembly entry, `_st_md_thread_start`, instead of the way +it starts on Linux and macOS. The code is in `md_win64.asm`, the `WIN64` branch of `md.h`, and +`st_thread_create` in `sched.c`. + +## A thread is a saved set of registers + +Running code is described by the CPU registers and the stack that `rsp` points into. To pause a thread, ST +saves the registers into its jmpbuf (`thread->context`); to resume it, ST loads them back. Two assembly +functions in `md_win64.asm` do this: + +- `_st_md_cxt_save(env)`, like `setjmp`, stores the registers into `env` and returns 0. +- `_st_md_cxt_restore(env, val)`, like `longjmp`, loads the registers from `env`, sets `rsp`, and jumps to + the saved PC. The CPU continues right after the `_st_md_cxt_save` call that filled `env`, as if that call + returned a second time, with `val` (1 if `val` is 0). + +The jmpbuf holds every register the Windows x64 ABI calls callee-saved, which a function must give back +unchanged, so the compiler may keep values in them across a call. A context switch is such a call. + +| Slot | Content | Why | +| --- | --- | --- | +| 0-7 | `rbx`, `rbp`, `rdi`, `rsi`, `r12`-`r15` | Callee-saved integer registers. On Linux, `rdi` and `rsi` are not. | +| 8 | `rsp` | The stack the thread runs on (`MD_GET_SP`). | +| 9 | PC | Where the thread continues: the return address of the save. | +| 10-12 | TIB `StackBase`, `StackLimit`, `DeallocationStack` | The bounds Windows keeps for the current stack (see below). | +| after 12 | MXCSR, x87 control word, `xmm6`-`xmm15` | Also callee-saved on Windows. On Linux, no `xmm` register is. | + +## Switching between two threads + +A thread that blocks saves itself and restores the next one: + +```c +if (_st_md_cxt_save(me->context) == 0) { /* save me, returns 0 */ + _st_md_cxt_restore(next->context, 1); /* jump into next */ +} +/* Later, when another thread restores me, the save returns 1 here and I go on. */ +``` + +The next thread was paused the same way, so its jmpbuf holds its own stack, PC, and registers, and it +continues inside a function that really runs on that stack. + +## Starting a new thread + +A new thread has never run, so it has never called `_st_md_cxt_save`; its first jmpbuf must be made up: a +stack pointer into its new, empty stack, and a PC to start at. + +### Linux and macOS: save, then patch the SP + +```c +if (_st_md_cxt_save(thread->context)) { /* 1. save the creator's registers */ + _st_thread_main(); /* 3. the new thread starts here */ +} +MD_GET_SP(thread) = stack->sp; /* 2. point only the SP at the new stack */ +``` + +The first restore of this jmpbuf jumps back into `st_thread_create`, the save returns 1, and the code calls +`_st_thread_main`. But that code now runs on the new stack, where `st_thread_create` was never called: + +``` +creator's stack new thread's stack ++---------------------------+ +---------------------------+ +| st_thread_create locals, | | empty | <- rsp after the restore +| saved registers, return | | | ++---------------------------+ +---------------------------+ +``` + +After the second return, a read of a local from the stack frame gets garbage, and a value the compiler kept +in a register from before the save is right only if it is one of the callee-saved registers the restore +loads back. The trick works only while the compiler emits nothing there but "if 1, call `_st_thread_main`". +The C language does not promise that, and the compiler does not know that `_st_md_cxt_save` returns twice. + +On Linux it holds in practice: an access to a `__thread` variable such as `_st_this_thread` is one +instruction (`mov %fs:offset` on x86-64, `tpidr_el0` on arm64), so there is nothing worth computing before +the save, and GCC and Clang have generated safe code there for decades. + +On Windows it held only by luck. With `/O2` and with `/GL` (LTCG), MSVC inlines `_st_thread_main` into +`st_thread_create`. A Windows TLS access takes several steps (`gs:[58h]`, the TLS array, `_tls_index`, the +offset), so MSVC computes the TLS base and `&_st_this_vp` before the save, keeps them in `r13` and `r14` +(`rbp` and `r15` with LTCG), and uses them after the second return. That is right only because those +registers are callee-saved; a spill to the stack would crash the new thread. + +### Windows: start in an assembly entry + +On `WIN64`, `MD_INIT_THREAD_ENTRY` runs in `st_thread_create` after the SP is patched, and points the +jmpbuf at a small function written in assembly, so no compiler decides what runs first on the new stack: + +```c +sp = align_down(stack->sp, 16) - 8; /* 8 mod 16, as at the entry of a function */ +*(void **)sp = NULL; /* a return address of 0 */ +jmpbuf[8] = sp; /* SP */ +jmpbuf[9] = _st_md_thread_start; /* PC */ +``` + +The new stack is then: + +``` +higher addresses: the thread object and its keys ++----------------------+ +| pad, 128 bytes | ++----------------------+ <- 16-byte aligned +| 0, return address | <- the SP in the jmpbuf, 8 mod 16 ++----------------------+ +| free stack, grows down +v +``` + +The first restore loads the registers, sets `rsp` to that slot, and jumps to `_st_md_thread_start`. To the +CPU this is a normal function call from address 0: a call pushes the return address and jumps, and the +"pushed" return address is the 0 written there. + +```asm +_st_md_thread_start PROC FRAME + sub rsp, 40 ; the callee's 32-byte home space, and 8 bytes to align rsp to 16 + .allocstack 40 ; unwind info: this frame is 40 bytes + .endprolog + call _st_thread_main ; run start(arg), then st_thread_exit + int 3 ; never reached; st_thread_exit never returns +_st_md_thread_start ENDP +``` + +- The Windows x64 ABI wants `rsp` 16-byte aligned at every `call`, so it is 8 mod 16 at the entry of a + function. The entry starts in exactly that state, and `sub rsp, 40` aligns it again for the call. A wrong + alignment breaks aligned SSE instructions such as `movaps`. +- `_st_thread_main` is ordinary C with a normal frame. It runs the thread function and then + `st_thread_exit`, which switches to another thread and never comes back. +- The new thread never runs a line of `st_thread_create`. The only code between the restore and + `_st_thread_main` is the four instructions above, and the registers the creator saved are left over and + never read. So the start no longer depends on how the compiler uses that frame or its registers after the + save returns twice, at `/Od`, `/O2`, or `/GL`. + +The restore itself is the same for every switch; only the first jmpbuf of a new thread differs. + +## Exceptions and stack walks (SEH) + +Structured Exception Handling (SEH) is how Windows handles errors raised while code runs: a hardware fault +such as an access violation is an SEH exception, and MSVC implements a C++ `throw` as an SEH exception too +(code `0xE06D7363`). To find a handler (a `catch` or an `__except`), Windows walks the stack frame by frame, +from the current function to its callers. Debuggers, crash dumps, and `RtlCaptureStackBackTrace` walk the +stack the same way. + +On x64, Windows does not follow `rbp` chains. Every function has an entry in the unwind tables (`.pdata` +and `.xdata`) that the compiler or assembler generates. For a PC, the entry tells how far the function moved +`rsp` and which registers it saved, so Windows finds the return address and steps to the caller. + +Windows also checks every step against the stack bounds in the thread information block (TIB). If `rsp` +leaves `StackLimit`..`StackBase`, it takes the stack as corrupt and ends the process. That is why the +context switch saves and loads the TIB bounds (slots 10-12), and `st_thread_create` sets them for a new +thread (`MD_INIT_STACK_BOUNDS`). Without them, any exception on a thread stack would look like a corrupt +stack. + +With the save-then-patch-SP trick, the outermost frame of a new thread would be code of `st_thread_create` +on a stack it was never called on. Its unwind entry points to a return address above the SP, in the pad or +the thread object, which is not a return address, so a walk would step into a random caller. With the assembly entry, the walk ends +cleanly: + +``` +thread function -> unwind info: returns to _st_thread_main +_st_thread_main -> unwind info: returns to _st_md_thread_start +_st_md_thread_start -> PROC FRAME, .allocstack 40: the return address is at rsp+40, it is 0 +0 -> no caller: the end of the stack +``` + +`PROC FRAME`, `.allocstack`, and `.endprolog` make `ml64` generate the unwind entry of the assembly +function; without them, Windows could not step over it. A return address of 0 is the usual mark of the +outermost frame. So a backtrace in a thread function has exactly three frames: the thread function, +`_st_thread_main`, and `_st_md_thread_start`. A C++ `throw` is caught across any depth of frames, and an +unhandled exception ends the process with its exception code instead of hanging. + +Linux has the same idea with DWARF `.eh_frame` instead of `.pdata`, but with no TIB check, and C++ code on +a thread stack normally catches its exceptions inside the thread function. + +## Summary + +| | Linux and macOS | Windows x64 | +| --- | --- | --- | +| PC of a new thread | In `st_thread_create`, after the save | `_st_md_thread_start`, in assembly | +| SP of a new thread | The new stack top | The new stack top, aligned, with a return address of 0 | +| First code on the new stack | The tail of `st_thread_create`, from the compiler | Four instructions written by hand | +| Depends on the optimizer | Yes, in theory | No | +| Stack walk at the top | Not defined | Ends at the return address of 0 | +| Registers saved | `rbx`, `rbp`, `r12`-`r15`, `rsp`, PC | Also `rdi`, `rsi`, `xmm6`-`xmm15`, MXCSR, x87 control word, TIB bounds | + +Only `WIN64` defines `MD_INIT_THREAD_ENTRY` now; the other platforms keep the save-then-patch-SP trick. A +later change may give every platform and CPU an assembly entry, so all start new threads the same way. diff --git a/event.c b/event.c index e7cbdbd..32e3e75 100644 --- a/event.c +++ b/event.c @@ -35,7 +35,11 @@ */ #include +#if !defined(WIN64) #include +#else +#include +#endif #include #include #include @@ -57,7 +61,7 @@ __thread unsigned long long _st_stat_epoll_shake = 0; __thread unsigned long long _st_stat_epoll_spin = 0; #endif -#if !defined(MD_HAVE_KQUEUE) && !defined(MD_HAVE_EPOLL) && !defined(MD_HAVE_SELECT) +#if !defined(MD_HAVE_KQUEUE) && !defined(MD_HAVE_EPOLL) && !defined(MD_HAVE_SELECT) && !defined(WIN64) #error Only support epoll(for Linux), kqueue(for Darwin) or select(for Cygwin) #endif @@ -1247,6 +1251,279 @@ static _st_eventsys_t _st_epoll_eventsys = { #endif /* MD_HAVE_EPOLL */ +#if defined (WIN64) +/***************************************** + * Native Windows event system with WSAPoll, for features, not performance. + * WSAPoll polls sockets only, has no POLLPRI, and fails instead of sleeping when + * given no sockets. Before Windows 10 2004, it does not report a failed connect, + * so such a connect waits until its timeout. + */ + +static __thread struct _st_wsapolldata { + WSAPOLLFD *fds; + int fds_size; +} *_st_wsapoll_data; + +ST_HIDDEN int _st_win64_init(void) +{ + _st_wsapoll_data = (struct _st_wsapolldata *) calloc(1, sizeof(*_st_wsapoll_data)); + if (!_st_wsapoll_data) { + errno = ENOMEM; + return -1; + } + + return 0; +} + +/* The WSAPoll timeout in milliseconds until the earliest sleeper is due, or -1 for none. */ +static int _st_win64_timeout_ms(void) +{ + st_utime_t min_timeout; + + if (_st_this_vp.sleep_q == NULL) + return -1; + + min_timeout = (_st_this_vp.sleep_q->due <= _st_this_vp.last_clock) ? 0 : + (_st_this_vp.sleep_q->due - _st_this_vp.last_clock); + /* Round up to milliseconds, so the sleeper is due when the dispatch returns. */ + min_timeout = (min_timeout + 999) / 1000; + return (min_timeout >= INT_MAX) ? INT_MAX : (int) min_timeout; +} + +/* The ST events of one WSAPoll result, like the epoll backend: an error or hang-up also wakes every waiter. */ +static short _st_win64_revents(short events, short r) +{ + short revents = 0; + + if (r & (POLLERR | POLLHUP | POLLNVAL)) + revents |= events; + if ((events & POLLIN) && (r & POLLIN)) + revents |= POLLIN; + if ((events & POLLOUT) && (r & POLLOUT)) + revents |= POLLOUT; + revents |= r & (POLLERR | POLLHUP | POLLNVAL); + + return revents; +} + +/* Wake the thread of a pollset whose descriptors are ready. */ +static void _st_win64_notify(_st_pollq_t *pq) +{ + st_clist_remove(&pq->links); + pq->on_ioq = 0; + + if (pq->thread->flags & _ST_FL_ON_SLEEPQ) + _st_del_sleep_q(pq->thread); + pq->thread->state = _ST_ST_RUNNABLE; + st_clist_insert_before(&pq->thread->links, &_st_this_vp.run_q); +} + +/* The whole WSAPoll failed, which a closed descriptor may cause: wake the pollsets with one, with POLLNVAL. */ +static void _st_win64_find_bad_fd(void) +{ + _st_clist_t *q; + _st_pollq_t *pq; + struct pollfd *pds, *epds; + int notify, type; + socklen_t len; + + for (q = _st_this_vp.io_q.next; q != &_st_this_vp.io_q; q = q->next) { + pq = _ST_POLLQUEUE_PTR(q); + notify = 0; + epds = pq->pds + pq->npds; + + for (pds = pq->pds; pds < epds; pds++) { + pds->revents = 0; + len = sizeof(type); + if (getsockopt((SOCKET)(intptr_t) pds->fd, SOL_SOCKET, SO_TYPE, (char *) &type, &len) != 0) { + pds->revents = POLLNVAL; + notify = 1; + } + } + + if (notify) + _st_win64_notify(pq); + } +} + +ST_HIDDEN void _st_win64_dispatch(void) +{ + _st_clist_t *q; + _st_pollq_t *pq; + struct pollfd *pds, *epds; + WSAPOLLFD *fds; + int n, i, nfd, notify, ms; + + /* Count the descriptors of every pollset on the I/O queue, and grow the array for them. */ + n = 0; + for (q = _st_this_vp.io_q.next; q != &_st_this_vp.io_q; q = q->next) + n += _ST_POLLQUEUE_PTR(q)->npds; + + if (n > _st_wsapoll_data->fds_size) { + fds = (WSAPOLLFD *) realloc(_st_wsapoll_data->fds, n * sizeof(WSAPOLLFD)); + if (fds) { + _st_wsapoll_data->fds = fds; + _st_wsapoll_data->fds_size = n; + } else { + /* Out of memory: wait for the sleepers only, and try again next time. */ + n = 0; + } + } + + ms = _st_win64_timeout_ms(); + if (n == 0) { + /* Nothing to poll, so only a timeout can wake a thread, like select() with no descriptors. */ + Sleep(ms < 0 ? INFINITE : (DWORD) ms); + return; + } + + fds = _st_wsapoll_data->fds; + i = 0; + for (q = _st_this_vp.io_q.next; q != &_st_this_vp.io_q; q = q->next) { + pq = _ST_POLLQUEUE_PTR(q); + epds = pq->pds + pq->npds; + for (pds = pq->pds; pds < epds; pds++, i++) { + fds[i].fd = (SOCKET)(intptr_t) pds->fd; + fds[i].events = pds->events; + fds[i].revents = 0; + } + } + + /* Check for I/O operations */ + nfd = WSAPoll(fds, (ULONG) n, ms); + + /* Notify threads that are associated with the selected descriptors, in the order the array was built. */ + if (nfd > 0) { + i = 0; + for (q = _st_this_vp.io_q.next; q != &_st_this_vp.io_q; ) { + pq = _ST_POLLQUEUE_PTR(q); + q = q->next; + notify = 0; + epds = pq->pds + pq->npds; + for (pds = pq->pds; pds < epds; pds++, i++) { + pds->revents = _st_win64_revents(pds->events, fds[i].revents); + if (pds->revents) + notify = 1; + } + if (notify) + _st_win64_notify(pq); + } + } else if (nfd < 0) { + if (WSAGetLastError() == WSAENOTSOCK) + _st_win64_find_bad_fd(); + } +} + +ST_HIDDEN int _st_win64_pollset_add(struct pollfd *pds, int npds) +{ + struct pollfd *pd; + struct pollfd *epd = pds + npds; + + /* + * Do checks up front, like the select backend. Winsock's pollfd has an unsigned SOCKET, so a negative + * descriptor is checked as the int ST keeps. Winsock's POLLIN is POLLRDNORM | POLLRDBAND, so a part of it, such + * as POLLRDNORM from a program written for poll(2), is refused too, and so is POLLPRI. + */ + for (pd = pds; pd < epd; pd++) { + if ((int) pd->fd < 0 || !pd->events || (pd->events & ~(POLLIN | POLLOUT)) || + ((pd->events & POLLIN) && (pd->events & POLLIN) != POLLIN)) { + errno = EINVAL; + return -1; + } + } + + /* Registered by the pollset on the I/O queue. */ + return 0; +} + +ST_HIDDEN void _st_win64_pollset_del(struct pollfd *pds, int npds) +{ + /* Unregistered by removing the pollset from the I/O queue. */ + (void) pds; + (void) npds; +} + +ST_HIDDEN int _st_win64_fd_new(int osfd) +{ + int type; + socklen_t len = sizeof(type); + + /* WSAPoll polls only sockets, so anything else, such as a closed socket, is a bad descriptor. */ + if (getsockopt((SOCKET)(intptr_t) osfd, SOL_SOCKET, SO_TYPE, (char *) &type, &len) != 0) { + errno = EBADF; + return -1; + } + + return 0; +} + +ST_HIDDEN int _st_win64_fd_close(int osfd) +{ + _st_clist_t *q; + _st_pollq_t *pq; + struct pollfd *pds, *epds; + + for (q = _st_this_vp.io_q.next; q != &_st_this_vp.io_q; q = q->next) { + pq = _ST_POLLQUEUE_PTR(q); + epds = pq->pds + pq->npds; + for (pds = pq->pds; pds < epds; pds++) { + if ((int) pds->fd == osfd) { + errno = EBUSY; + return -1; + } + } + } + + return 0; +} + +ST_HIDDEN int _st_win64_fd_getlimit(void) +{ + /* No limit of the event system itself. */ + return 0; +} + +ST_HIDDEN void _st_win64_destroy(void) +{ + if (_st_wsapoll_data) { + free(_st_wsapoll_data->fds); + free(_st_wsapoll_data); + _st_wsapoll_data = NULL; + } +} + +/* + * One backend for both choices (D8): ST_EVENTSYS_DEFAULT and ST_EVENTSYS_SELECT give the + * select-like default, and ST_EVENTSYS_ALT is accepted, so callers that ask for it work. + */ +static _st_eventsys_t _st_win64_eventsys = { + "wsapoll", + ST_EVENTSYS_SELECT, + _st_win64_init, + _st_win64_dispatch, + _st_win64_pollset_add, + _st_win64_pollset_del, + _st_win64_fd_new, + _st_win64_fd_close, + _st_win64_fd_getlimit, + _st_win64_destroy +}; + +static _st_eventsys_t _st_win64_alt_eventsys = { + "wsapoll", + ST_EVENTSYS_ALT, + _st_win64_init, + _st_win64_dispatch, + _st_win64_pollset_add, + _st_win64_pollset_del, + _st_win64_fd_new, + _st_win64_fd_close, + _st_win64_fd_getlimit, + _st_win64_destroy +}; +#endif /* WIN64 */ + + /***************************************** * Public functions */ @@ -1258,6 +1535,17 @@ int st_set_eventsys(int eventsys) return -1; } +#if defined (WIN64) + if (eventsys == ST_EVENTSYS_SELECT || eventsys == ST_EVENTSYS_DEFAULT) { + _st_eventsys = &_st_win64_eventsys; + return 0; + } + if (eventsys == ST_EVENTSYS_ALT) { + _st_eventsys = &_st_win64_alt_eventsys; + return 0; + } +#endif + if (eventsys == ST_EVENTSYS_SELECT || eventsys == ST_EVENTSYS_DEFAULT) { #if defined (MD_HAVE_SELECT) _st_eventsys = &_st_select_eventsys; diff --git a/io.c b/io.c index 2d2ba1d..c42b100 100644 --- a/io.c +++ b/io.c @@ -42,18 +42,467 @@ */ #include +#if !defined(WIN64) #include +#endif #include +#if !defined(WIN64) #include #include #include #include #include +#endif #include #include #include #include "common.h" +#if defined(WIN64) +/* + * Native Windows implements the POSIX calls of this file with Winsock, for sockets + * only, so the library brings ws2_32.lib. st_open (files and FIFOs) is not supported. + */ +#include +#include +#pragma comment(lib, "ws2_32.lib") + +/* + * Map a Winsock error to errno, which ST and its callers such as SRS check. For + * connect (connecting is set), WSAEWOULDBLOCK means EINPROGRESS. An error with no + * equivalent is EIO; WSAGetLastError() still has the Winsock error. + */ +int _st_win64_errno(int wsaerr, int connecting) +{ + switch (wsaerr) { + case 0: return 0; + case WSAEWOULDBLOCK: return connecting ? EINPROGRESS : EAGAIN; + case WSAEINPROGRESS: return EINPROGRESS; + case WSAEALREADY: return EALREADY; + case WSAEINTR: return EINTR; + case WSAETIMEDOUT: return ETIMEDOUT; + case WSAEBADF: return EBADF; + case WSAEACCES: return EACCES; + case WSAEFAULT: return EFAULT; + case WSAEINVAL: return EINVAL; + case WSAEMFILE: return EMFILE; + case WSAENOTSOCK: return ENOTSOCK; + case WSAEDESTADDRREQ: return EDESTADDRREQ; + case WSAEMSGSIZE: return EMSGSIZE; + case WSAEPROTOTYPE: return EPROTOTYPE; + case WSAENOPROTOOPT: return ENOPROTOOPT; + case WSAEPROTONOSUPPORT: return EPROTONOSUPPORT; + case WSAESOCKTNOSUPPORT: return EPROTONOSUPPORT; + case WSAEOPNOTSUPP: return EOPNOTSUPP; + case WSAEPFNOSUPPORT: return EAFNOSUPPORT; + case WSAEAFNOSUPPORT: return EAFNOSUPPORT; + case WSAEADDRINUSE: return EADDRINUSE; + case WSAEADDRNOTAVAIL: return EADDRNOTAVAIL; + case WSAENETDOWN: return ENETDOWN; + case WSAENETUNREACH: return ENETUNREACH; + case WSAENETRESET: return ENETRESET; + case WSAECONNABORTED: return ECONNABORTED; + case WSAECONNRESET: return ECONNRESET; + case WSAENOBUFS: return ENOBUFS; + case WSAEISCONN: return EISCONN; + case WSAENOTCONN: return ENOTCONN; + case WSAESHUTDOWN: return EPIPE; + case WSAECONNREFUSED: return ECONNREFUSED; + case WSAELOOP: return ELOOP; + case WSAENAMETOOLONG: return ENAMETOOLONG; + case WSAEHOSTDOWN: return EHOSTUNREACH; + case WSAEHOSTUNREACH: return EHOSTUNREACH; + case WSAENOTEMPTY: return ENOTEMPTY; + case WSA_INVALID_HANDLE: return EBADF; + case WSA_NOT_ENOUGH_MEMORY: return ENOMEM; + case WSA_INVALID_PARAMETER: return EINVAL; + case WSA_OPERATION_ABORTED: return ECANCELED; + case WSANOTINITIALISED: return EINVAL; + default: return EIO; + } +} + +/* Set errno from the Winsock error of the failed call, and return -1. */ +static int _st_win64_sock_fail(int connecting) +{ + errno = _st_win64_errno(WSAGetLastError(), connecting); + return -1; +} + +/* + * The Winsock calls ST makes, with errno set on failure. A SOCKET fits in an int + * (only 32 bits are significant), so it is truncated and sign-extended back. + */ +static int _st_win64_accept(int fd, struct sockaddr *addr, socklen_t *addrlen) +{ + SOCKET s = accept((SOCKET)fd, addr, addrlen); + return (s == INVALID_SOCKET) ? _st_win64_sock_fail(0) : (int)s; +} + +static int _st_win64_connect(int fd, const struct sockaddr *addr, int addrlen) +{ + return (connect((SOCKET)fd, addr, addrlen) == SOCKET_ERROR) ? _st_win64_sock_fail(1) : 0; +} + +/* SO_ERROR, which st_connect reads after the wait, is a Winsock error, so it gives the errno instead. */ +static int _st_win64_getsockopt(int fd, int level, int name, char *value, socklen_t *size) +{ + if (getsockopt((SOCKET)fd, level, name, value, size) == SOCKET_ERROR) + return _st_win64_sock_fail(0); + if (level == SOL_SOCKET && name == SO_ERROR && *size == sizeof(int) && *(int *)value) + *(int *)value = _st_win64_errno(*(int *)value, 1); + return 0; +} + +/* + * Set errno from the Winsock error of a failed receive, and return -1. Winsock reports + * an ICMP port unreachable on a datagram socket as WSAECONNRESET, where POSIX gives + * ECONNREFUSED. + */ +static int _st_win64_recv_sock_fail(int fd) +{ + int err = WSAGetLastError(); + int type = 0; + socklen_t size = sizeof(type); + + if (err == WSAECONNRESET && getsockopt((SOCKET)fd, SOL_SOCKET, SO_TYPE, (char *)&type, &size) == 0 && + type == SOCK_DGRAM) { + WSASetLastError(err); + errno = ECONNREFUSED; + return -1; + } + WSASetLastError(err); + errno = _st_win64_errno(err, 0); + return -1; +} + +int _st_win64_recvfrom(int fd, void *buf, int len, int flags, struct sockaddr *from, socklen_t *fromlen) +{ + int n = recvfrom((SOCKET)fd, (char *)buf, len, flags, from, fromlen); + if (n != SOCKET_ERROR) + return n; + /* A datagram larger than the buffer: POSIX returns the bytes that fit and drops the rest. */ + if (WSAGetLastError() == WSAEMSGSIZE) + return len; + return _st_win64_recv_sock_fail(fd); +} + +static int _st_win64_sendto(int fd, const void *msg, int len, int flags, const struct sockaddr *to, int tolen) +{ + int n = sendto((SOCKET)fd, (const char *)msg, len, flags, to, tolen); + return (n == SOCKET_ERROR) ? _st_win64_sock_fail(0) : n; +} + +/* The fcntl commands and flag ST uses; the CRT has none for sockets. */ +#define F_GETFL 3 +#define F_SETFL 4 +#define O_NONBLOCK 04000 + +static int _st_win64_enosys(void) +{ + errno = ENOSYS; + return -1; +} + +/* ioctl is ioctlsocket, for FIONBIO. */ +int _st_win64_ioctl(int fd, unsigned long request, int *arg) +{ + u_long v = (u_long)*arg; + if (ioctlsocket((SOCKET)fd, (long)request, &v) == SOCKET_ERROR) + return _st_win64_sock_fail(0); + *arg = (int)v; + return 0; +} + +/* + * fcntl supports only sockets and only the non-blocking flag. Windows cannot read + * the mode of a socket back, so F_GETFL checks that fd is a socket and gives 0. + */ +int _st_win64_fcntl(int fd, int cmd, int arg) +{ + u_long nonblock; + int type; + socklen_t size = sizeof(type); + + switch (cmd) { + case F_GETFL: + if (getsockopt((SOCKET)fd, SOL_SOCKET, SO_TYPE, (char *)&type, &size) == SOCKET_ERROR) + return _st_win64_sock_fail(0); + return 0; + case F_SETFL: + nonblock = (arg & O_NONBLOCK) ? 1 : 0; + if (ioctlsocket((SOCKET)fd, FIONBIO, &nonblock) == SOCKET_ERROR) + return _st_win64_sock_fail(0); + return 0; + default: + errno = EINVAL; + return -1; + } +} + +int _st_win64_close(int fd) +{ + return (closesocket((SOCKET)fd) == SOCKET_ERROR) ? _st_win64_sock_fail(0) : 0; +} + +/* Winsock takes an int length; a larger request reads or writes less, as POSIX allows. */ +static int _st_win64_len(size_t nbyte) +{ + return (nbyte > INT_MAX) ? INT_MAX : (int)nbyte; +} + +ssize_t _st_win64_read(int fd, void *buf, size_t nbyte) +{ + int len = _st_win64_len(nbyte); + int n = recv((SOCKET)fd, (char *)buf, len, 0); + if (n != SOCKET_ERROR) + return n; + /* A datagram larger than the buffer: POSIX returns the bytes that fit and drops the rest. */ + if (WSAGetLastError() == WSAEMSGSIZE) + return len; + return _st_win64_recv_sock_fail(fd); +} + +ssize_t _st_win64_write(int fd, const void *buf, size_t nbyte) +{ + int n = send((SOCKET)fd, (const char *)buf, _st_win64_len(nbyte), 0); + return (n == SOCKET_ERROR) ? _st_win64_sock_fail(0) : n; +} + +/* The WSABUFs kept on the stack; more are allocated. */ +#define _ST_WIN64_LOCAL_BUFS 16 + +/* + * Copy the iovecs to WSABUFs, which are not the same layout (length first, and a + * 32-bit length), so never cast one to the other. Uses local when it is large + * enough, or allocates the array, which the caller frees with _st_win64_bufs_free. + * A vector longer than a WSABUF holds is cut, with the vectors after it, so the + * call sends or receives less, as POSIX allows. Returns NULL with errno on error. + */ +static WSABUF *_st_win64_bufs(const struct iovec *iov, int iov_size, WSABUF *local, DWORD *count, size_t *total) +{ + WSABUF *bufs = local; + int i; + + if (iov_size < 0 || (iov_size > 0 && !iov)) { + errno = EINVAL; + return NULL; + } + if (iov_size > _ST_WIN64_LOCAL_BUFS) { + if ((bufs = (WSABUF *)malloc(iov_size * sizeof(WSABUF))) == NULL) { + errno = ENOMEM; + return NULL; + } + } + + *total = 0; + for (i = 0; i < iov_size; i++) { + int cut = iov[i].iov_len > ULONG_MAX; + bufs[i].len = cut ? ULONG_MAX : (ULONG)iov[i].iov_len; + bufs[i].buf = (char *)iov[i].iov_base; + *total += bufs[i].len; + if (cut) { + i++; + break; + } + } + *count = (DWORD)i; + return bufs; +} + +static void _st_win64_bufs_free(WSABUF *bufs, WSABUF *local) +{ + if (bufs != local) + free(bufs); +} + +/* The result of a receive that failed: a truncated datagram is the bytes that fit. */ +static ssize_t _st_win64_recv_fail(int fd, size_t total) +{ + if (WSAGetLastError() == WSAEMSGSIZE) + return (ssize_t)total; + return _st_win64_recv_sock_fail(fd); +} + +ssize_t _st_win64_readv(int fd, const struct iovec *iov, int iov_size) +{ + WSABUF local[_ST_WIN64_LOCAL_BUFS], *bufs; + DWORD count, nbytes = 0, flags = 0; + size_t total; + ssize_t rv; + + if (iov_size == 0) + return 0; + if ((bufs = _st_win64_bufs(iov, iov_size, local, &count, &total)) == NULL) + return -1; + if (WSARecv((SOCKET)fd, bufs, count, &nbytes, &flags, NULL, NULL) == 0) + rv = (ssize_t)nbytes; + else + rv = _st_win64_recv_fail(fd, total); + _st_win64_bufs_free(bufs, local); + return rv; +} + +ssize_t _st_win64_writev(int fd, const struct iovec *iov, int iov_size) +{ + WSABUF local[_ST_WIN64_LOCAL_BUFS], *bufs; + DWORD count, nbytes = 0; + size_t total; + ssize_t rv; + + if (iov_size == 0) + return 0; + if ((bufs = _st_win64_bufs(iov, iov_size, local, &count, &total)) == NULL) + return -1; + if (WSASend((SOCKET)fd, bufs, count, &nbytes, 0, NULL, NULL) == 0) + rv = (ssize_t)nbytes; + else + rv = _st_win64_sock_fail(0); + _st_win64_bufs_free(bufs, local); + return rv; +} + +/* + * WSARecvMsg is a Winsock extension, loaded once with WSAIoctl. The Microsoft + * providers all return the same function, so one pointer serves every socket. + */ +static LPFN_WSARECVMSG _st_win64_wsarecvmsg = NULL; + +static LPFN_WSARECVMSG _st_win64_load_wsarecvmsg(int fd) +{ + GUID guid = WSAID_WSARECVMSG; + LPFN_WSARECVMSG fn = NULL; + DWORD n = 0; + + if (_st_win64_wsarecvmsg) + return _st_win64_wsarecvmsg; + if (WSAIoctl((SOCKET)fd, SIO_GET_EXTENSION_FUNCTION_POINTER, &guid, sizeof(guid), &fn, sizeof(fn), &n, + NULL, NULL) == SOCKET_ERROR) { + _st_win64_sock_fail(0); + return NULL; + } + _st_win64_wsarecvmsg = fn; + return fn; +} + +/* + * WSARecvMsg and WSASendMsg take only datagram and raw sockets, while POSIX recvmsg + * and sendmsg take streams too. After such a failure, this tells whether fd is a + * stream, which then uses WSARecv or WSASend. + */ +static int _st_win64_is_stream(int fd, int wsaerr) +{ + int type = 0; + socklen_t size = sizeof(type); + + if (wsaerr != WSAEINVAL && wsaerr != WSAEOPNOTSUPP) + return 0; + if (getsockopt((SOCKET)fd, SOL_SOCKET, SO_TYPE, (char *)&type, &size) == SOCKET_ERROR) + return 0; + WSASetLastError(wsaerr); + return type == SOCK_STREAM; +} + +/* + * recvmsg with WSARecvMsg. The control data is Winsock's (WSA_CMSG_* macros, for + * example IP_PKTINFO), not the POSIX layout. A truncated datagram returns the bytes + * that fit with MSG_TRUNC, as on POSIX, where Winsock fails with WSAEMSGSIZE. + */ +int _st_win64_recvmsg(int fd, struct msghdr *msg, int flags) +{ + WSABUF local[_ST_WIN64_LOCAL_BUFS], *bufs; + LPFN_WSARECVMSG fn; + WSAMSG wmsg; + DWORD count, nbytes = 0, dflags; + size_t total; + int rv, err; + + if ((fn = _st_win64_load_wsarecvmsg(fd)) == NULL) + return -1; + if ((bufs = _st_win64_bufs(msg->msg_iov, msg->msg_iovlen, local, &count, &total)) == NULL) + return -1; + + wmsg.name = (LPSOCKADDR)msg->msg_name; + wmsg.namelen = msg->msg_name ? msg->msg_namelen : 0; + wmsg.lpBuffers = bufs; + wmsg.dwBufferCount = count; + wmsg.Control.buf = (char *)msg->msg_control; + wmsg.Control.len = msg->msg_control ? (ULONG)msg->msg_controllen : 0; + wmsg.dwFlags = (DWORD)flags; + + if ((*fn)((SOCKET)fd, &wmsg, &nbytes, NULL, NULL) == 0) { + rv = (int)nbytes; + } else if ((err = WSAGetLastError()) == WSAEMSGSIZE) { + rv = (int)total; + wmsg.dwFlags |= MSG_TRUNC; + } else if (_st_win64_is_stream(fd, err)) { + /* A stream has no sender address and no control data. */ + dflags = (DWORD)flags; + rv = (WSARecv((SOCKET)fd, bufs, count, &nbytes, &dflags, NULL, NULL) == 0) ? + (int)nbytes : _st_win64_sock_fail(0); + wmsg.namelen = 0; + wmsg.Control.len = 0; + wmsg.dwFlags = 0; + } else { + rv = _st_win64_recv_sock_fail(fd); + } + + if (rv >= 0) { + msg->msg_namelen = wmsg.namelen; + msg->msg_controllen = wmsg.Control.len; + msg->msg_flags = (int)wmsg.dwFlags; + } + _st_win64_bufs_free(bufs, local); + return rv; +} + +/* sendmsg with WSASendMsg; the control data is Winsock's, as for recvmsg. */ +int _st_win64_sendmsg(int fd, const struct msghdr *msg, int flags) +{ + WSABUF local[_ST_WIN64_LOCAL_BUFS], *bufs; + WSAMSG wmsg; + DWORD count, nbytes = 0; + size_t total; + int rv; + + if ((bufs = _st_win64_bufs(msg->msg_iov, msg->msg_iovlen, local, &count, &total)) == NULL) + return -1; + + wmsg.name = (LPSOCKADDR)msg->msg_name; + wmsg.namelen = msg->msg_name ? msg->msg_namelen : 0; + wmsg.lpBuffers = bufs; + wmsg.dwBufferCount = count; + wmsg.Control.buf = (char *)msg->msg_control; + wmsg.Control.len = msg->msg_control ? (ULONG)msg->msg_controllen : 0; + wmsg.dwFlags = 0; + + if (WSASendMsg((SOCKET)fd, &wmsg, (DWORD)flags, &nbytes, NULL, NULL) == 0) + rv = (int)nbytes; + else if (_st_win64_is_stream(fd, WSAGetLastError())) + rv = (WSASend((SOCKET)fd, bufs, count, &nbytes, (DWORD)flags, NULL, NULL) == 0) ? + (int)nbytes : _st_win64_sock_fail(0); + else + rv = _st_win64_sock_fail(0); + _st_win64_bufs_free(bufs, local); + return rv; +} + +#define ioctl _st_win64_ioctl +#define fcntl _st_win64_fcntl +#define close _st_win64_close +#define read _st_win64_read +#define write _st_win64_write +#define readv _st_win64_readv +#define writev _st_win64_writev +#define recvmsg _st_win64_recvmsg +#define sendmsg _st_win64_sendmsg +#define accept _st_win64_accept +#define connect _st_win64_connect +#define getsockopt _st_win64_getsockopt +#define recvfrom _st_win64_recvfrom +#define sendto _st_win64_sendto +#endif + // Global stat. #if defined(DEBUG) && defined(DEBUG_STATS) __thread unsigned long long _st_stat_recvfrom = 0; @@ -87,6 +536,52 @@ static int _st_osfd_limit = -1; static void _st_netfd_free_aux_data(_st_netfd_t *fd); +#if defined(WIN64) +/* + * Windows has no SIGPIPE and no rlimit. A process may open up to 2^24 handles, sockets + * included, so report that unless the event system has a lower limit. + */ +#define _ST_WIN64_OSFD_LIMIT (1 << 24) + +/* Whether this thread's st_init started Winsock, which st_destroy cleans up. */ +static __thread int _st_wsa_started = 0; + +int _st_io_init(void) +{ + int fdlim; + + /* + * Winsock needs WSAStartup before any socket call. It counts the starts, so each + * thread's ST starts it once and st_destroy cleans up that start. + */ + if (!_st_wsa_started) { + WSADATA wsa; + int err = WSAStartup(MAKEWORD(2, 2), &wsa); + if (err != 0) { + errno = _st_win64_errno(err, 0); + return -1; + } + if (LOBYTE(wsa.wVersion) != 2 || HIBYTE(wsa.wVersion) != 2) { + WSACleanup(); + errno = EINVAL; + return -1; + } + _st_wsa_started = 1; + } + + fdlim = (*_st_eventsys->fd_getlimit)(); + _st_osfd_limit = (fdlim > 0 && fdlim < _ST_WIN64_OSFD_LIMIT) ? fdlim : _ST_WIN64_OSFD_LIMIT; + return 0; +} + +void _st_io_destroy(void) +{ + if (_st_wsa_started) { + WSACleanup(); + _st_wsa_started = 0; + } +} +#else int _st_io_init(void) { struct sigaction sigact; @@ -126,6 +621,7 @@ int _st_io_init(void) return 0; } +#endif int st_getfdlimit(void) @@ -736,6 +1232,15 @@ int st_sendmsg(_st_netfd_t *fd, const struct msghdr *msg, int flags, st_utime_t /* * To open FIFOs or other special files. */ +#if defined(WIN64) +/* Not supported on Windows yet. */ +_st_netfd_t *st_open(const char *path, int oflags, mode_t mode) +{ + (void) path; (void) oflags; (void) mode; + _st_win64_enosys(); + return NULL; +} +#else _st_netfd_t *st_open(const char *path, int oflags, mode_t mode) { int osfd, err; @@ -758,3 +1263,4 @@ _st_netfd_t *st_open(const char *path, int oflags, mode_t mode) return newfd; } +#endif diff --git a/md.h b/md.h index a25c008..20bb1de 100644 --- a/md.h +++ b/md.h @@ -70,8 +70,14 @@ typedef struct _st_jmp_buf { * Linux __riscv long[14] * Linux __loongarch64 long[12] * Cygwin64 __amd64__/__x86_64__ long[8] + * Win64 _M_X64 long long[36] */ - long __jmpbuf[22]; + /* Pointer-sized slots, because MSVC long is 32-bit (LLP64). */ +#if defined(WIN64) + intptr_t __jmpbuf[36]; +#else + intptr_t __jmpbuf[22]; +#endif } _st_jmp_buf_t[1]; /* Defined in *.S file and implemented by ASM. */ @@ -183,6 +189,78 @@ extern void _st_md_cxt_restore(_st_jmp_buf_t env, int val); (void) gettimeofday(&tv, NULL); \ return (tv.tv_sec * 1000000LL + tv.tv_usec) +#elif defined (WIN64) + + #define MD_ACCEPT_NB_NOT_INHERITED + #define MD_HAVE_SOCKLEN_T + + /* + * Stacks come from VirtualAlloc, with VirtualProtect red zones in DEBUG (stk.c). With + * MALLOC_STACK there are no red zones: VirtualProtect rounds a heap address down to its + * page, so it would protect heap memory next to the stack, where mprotect fails instead. + */ + #if defined(MALLOC_STACK) && !defined(MD_NO_PROTECT) + #define MD_NO_PROTECT + #endif + + /* The CRT has rand and srand, not random and srandom. */ + #define random rand + #define srandom srand + + /* MSVC has no __thread; use its thread-local storage class. */ + #define __thread __declspec(thread) + + #if defined(_M_X64) || defined(_M_AMD64) + #define MD_GET_SP(_t) *((long long *)&((_t)->context[0].__jmpbuf[8])) + /* + * The TIB stack bounds of a new thread, restored from slots 10-12 by md_win64.asm: + * StackBase is the stack top, StackLimit and DeallocationStack are the stack bottom. + * Otherwise the thread runs with its creator's bounds, which breaks C++ exceptions, + * stack walks, and __chkstk for frames over a page. + */ + #define MD_INIT_STACK_BOUNDS(_t, _bottom, _top) do { \ + (_t)->context[0].__jmpbuf[10] = (long long)(intptr_t)(_top); \ + (_t)->context[0].__jmpbuf[11] = (long long)(intptr_t)(_bottom); \ + (_t)->context[0].__jmpbuf[12] = (long long)(intptr_t)(_bottom); \ + } while (0) + /* + * A new thread starts in _st_md_thread_start (md_win64.asm), which calls _st_thread_main, + * instead of after _st_md_cxt_save in st_thread_create, so it does not depend on how the + * compiler uses that frame and its registers there. The SP (slot 8) moves 16-byte aligned + * minus 8 to a null return address, as at a function entry, and the PC (slot 9) is set. + */ + extern void _st_md_thread_start(void); + #define MD_INIT_THREAD_ENTRY(_t) do { \ + char *_sp = (char *)(intptr_t)MD_GET_SP(_t); \ + _sp = (char *)((intptr_t)_sp & ~(intptr_t)15) - sizeof(void *); \ + *(void **)_sp = NULL; \ + MD_GET_SP(_t) = (long long)(intptr_t)_sp; \ + (_t)->context[0].__jmpbuf[9] = (long long)(intptr_t)_st_md_thread_start; \ + } while (0) + #else + #error Unknown CPU architecture + #endif + + /* + * Like CLOCK_MONOTONIC on Linux: QueryPerformanceCounter, the clock that MSVC's + * std::chrono::steady_clock uses. Convert whole seconds and the leftover ticks + * apart, as steady_clock does, because ticks * 1000000 overflows after about + * 10 days of uptime at the common 10 MHz frequency. + */ + #define MD_GET_UTIME() \ + LARGE_INTEGER counter, freq; \ + QueryPerformanceCounter(&counter); \ + QueryPerformanceFrequency(&freq); \ + return (st_utime_t)((counter.QuadPart / freq.QuadPart) * 1000000LL + \ + (counter.QuadPart % freq.QuadPart) * 1000000LL / freq.QuadPart) + + static inline int getpagesize(void) + { + SYSTEM_INFO si; + GetSystemInfo(&si); + return (int)si.dwPageSize; + } + #else #error Unknown OS #endif /* OS */ diff --git a/md_win64.asm b/md_win64.asm new file mode 100644 index 0000000..c64c963 --- /dev/null +++ b/md_win64.asm @@ -0,0 +1,139 @@ +; SPDX-License-Identifier: MIT +; Copyright (c) 2013-2026 The SRS Authors + +; Native Windows x64 (MSVC, MASM) context switch, the WIN64 platform, not CYGWIN64. +; Assemble with: ml64 -nologo -c -Fomd_win64.o md_win64.asm +; +; The Win64 ABI callee-saved (nonvolatile) registers are rbx, rbp, rdi, rsi, r12-r15, +; xmm6-xmm15, the MXCSR control bits and the x87 control word, so all are saved. The +; TIB stack bounds (StackBase gs:[8], StackLimit gs:[16], DeallocationStack gs:[1478h]) +; are switched too, because the exception unwinder, __chkstk, and stack walks reject a +; stack outside these bounds. +; @see https://learn.microsoft.com/en-us/cpp/build/x64-software-conventions#register-usage +; +; The jmpbuf layout, in bytes, must match MD_GET_SP and _st_jmp_buf_t in md.h: +; 0 rbx, 8 rbp, 16 rdi, 24 rsi, 32-56 r12-r15, 64 rsp (slot 8), 72 pc (slot 9), +; 80 StackBase, 88 StackLimit, 96 DeallocationStack, 104 mxcsr, 108 x87 control word, +; 128-287 xmm6-xmm15 (unaligned, 16 bytes each). +JB_RBX EQU 0 +JB_RBP EQU 8 +JB_RDI EQU 16 +JB_RSI EQU 24 +JB_R12 EQU 32 +JB_R13 EQU 40 +JB_R14 EQU 48 +JB_R15 EQU 56 +JB_RSP EQU 64 +JB_PC EQU 72 +JB_STKBASE EQU 80 +JB_STKLIMIT EQU 88 +JB_STKDEALL EQU 96 +JB_MXCSR EQU 104 +JB_FPUCW EQU 108 +JB_XMM6 EQU 128 + +; The TIB (NT_TIB of the TEB) offsets from gs. +TIB_STKBASE EQU 8 +TIB_STKLIMIT EQU 16 +TIB_STKDEALL EQU 1478h + +.code + +; int _st_md_cxt_save(_st_jmp_buf_t env), env is rcx. Returns 0. +_st_md_cxt_save PROC + mov [rcx+JB_RBX], rbx + mov [rcx+JB_RBP], rbp + mov [rcx+JB_RDI], rdi + mov [rcx+JB_RSI], rsi + mov [rcx+JB_R12], r12 + mov [rcx+JB_R13], r13 + mov [rcx+JB_R14], r14 + mov [rcx+JB_R15], r15 + ; The SP of the caller, after this function returns. + lea rdx, [rsp+8] + mov [rcx+JB_RSP], rdx + ; The PC this function returns to. + mov rdx, [rsp] + mov [rcx+JB_PC], rdx + ; The TIB stack bounds. + mov rdx, gs:[TIB_STKBASE] + mov [rcx+JB_STKBASE], rdx + mov rdx, gs:[TIB_STKLIMIT] + mov [rcx+JB_STKLIMIT], rdx + mov rdx, gs:[TIB_STKDEALL] + mov [rcx+JB_STKDEALL], rdx + ; The floating-point control state. + stmxcsr dword ptr [rcx+JB_MXCSR] + fnstcw word ptr [rcx+JB_FPUCW] + movdqu [rcx+JB_XMM6+0], xmm6 + movdqu [rcx+JB_XMM6+16], xmm7 + movdqu [rcx+JB_XMM6+32], xmm8 + movdqu [rcx+JB_XMM6+48], xmm9 + movdqu [rcx+JB_XMM6+64], xmm10 + movdqu [rcx+JB_XMM6+80], xmm11 + movdqu [rcx+JB_XMM6+96], xmm12 + movdqu [rcx+JB_XMM6+112], xmm13 + movdqu [rcx+JB_XMM6+128], xmm14 + movdqu [rcx+JB_XMM6+144], xmm15 + xor eax, eax + ret +_st_md_cxt_save ENDP + +; void _st_md_cxt_restore(_st_jmp_buf_t env, int val), env is rcx, val is edx. +; Jumps back to the save of env, which then returns val, or 1 if val is 0. +_st_md_cxt_restore PROC + mov rbx, [rcx+JB_RBX] + mov rbp, [rcx+JB_RBP] + mov rdi, [rcx+JB_RDI] + mov rsi, [rcx+JB_RSI] + mov r12, [rcx+JB_R12] + mov r13, [rcx+JB_R13] + mov r14, [rcx+JB_R14] + mov r15, [rcx+JB_R15] + mov r8, [rcx+JB_STKBASE] + mov gs:[TIB_STKBASE], r8 + mov r8, [rcx+JB_STKLIMIT] + mov gs:[TIB_STKLIMIT], r8 + mov r8, [rcx+JB_STKDEALL] + mov gs:[TIB_STKDEALL], r8 + ldmxcsr dword ptr [rcx+JB_MXCSR] + fldcw word ptr [rcx+JB_FPUCW] + movdqu xmm6, [rcx+JB_XMM6+0] + movdqu xmm7, [rcx+JB_XMM6+16] + movdqu xmm8, [rcx+JB_XMM6+32] + movdqu xmm9, [rcx+JB_XMM6+48] + movdqu xmm10, [rcx+JB_XMM6+64] + movdqu xmm11, [rcx+JB_XMM6+80] + movdqu xmm12, [rcx+JB_XMM6+96] + movdqu xmm13, [rcx+JB_XMM6+112] + movdqu xmm14, [rcx+JB_XMM6+128] + movdqu xmm15, [rcx+JB_XMM6+144] + ; Return val, or 1 if val is 0. + mov eax, 1 + test edx, edx + cmovne eax, edx + ; Restore SP and jump to the saved PC. + mov r8, [rcx+JB_PC] + mov rsp, [rcx+JB_RSP] + jmp r8 +_st_md_cxt_restore ENDP + +EXTERN _st_thread_main:PROC + +; void _st_md_thread_start(void), the entry of a new thread, never returns. +; MD_INIT_THREAD_ENTRY in md.h points the PC of the new thread's jmpbuf here, and its SP at a +; null return address, 8 mod 16 as at any function entry, so _st_md_cxt_restore enters this +; function as if called from address 0, which ends stack walks. The thread no longer resumes +; after _st_md_cxt_save in st_thread_create, so it does not depend on how the compiler uses +; that frame or its registers after save returns twice (/Od, /O2, /GL). +_st_md_thread_start PROC FRAME + ; The 32-byte home space for the callee, and 8 bytes to align rsp to 16 at the call. + sub rsp, 40 + .allocstack 40 + .endprolog + call _st_thread_main + ; _st_thread_main calls st_thread_exit, which never returns. + int 3 +_st_md_thread_start ENDP + +END diff --git a/public.h b/public.h index 4b68a0e..b3b8c31 100644 --- a/public.h +++ b/public.h @@ -39,6 +39,62 @@ #ifndef __ST_THREAD_H__ #define __ST_THREAD_H__ +#if defined(_WIN32) || defined(_MSC_VER) +/* + * Native Windows with MSVC, which defines _WIN32 and _MSC_VER. Cygwin GCC + * defines neither. + */ + +/* + * st.h includes winsock2.h, but windows.h includes the old winsock.h, and the + * two cannot be used together. So include st.h before windows.h: + * #include + * #include + * Or define WIN32_LEAN_AND_MEAN, so windows.h does not include winsock.h: + * #define WIN32_LEAN_AND_MEAN + * #include + * #include + * In the wrong order, the build fails anyway with many redefinition errors in + * the Windows headers, so fail here first with a clear message. + */ +#if defined(_WINSOCKAPI_) && !defined(_WINSOCK2API_) && defined(IPPROTO_TCP) +#error "winsock.h is already included, by windows.h: include st.h before windows.h, or define WIN32_LEAN_AND_MEAN" +#endif + +#include +#include +#include +#include +#include +#include +#include + +#ifndef _SSIZE_T_DEFINED +#define _SSIZE_T_DEFINED +typedef intptr_t ssize_t; +#endif + +#ifndef _MODE_T_DEFINED +#define _MODE_T_DEFINED +typedef int mode_t; +#endif + +struct iovec { + void *iov_base; + size_t iov_len; +}; + +struct msghdr { + void *msg_name; + socklen_t msg_namelen; + struct iovec *msg_iov; + int msg_iovlen; + void *msg_control; + socklen_t msg_controllen; + int msg_flags; +}; +#else +/* POSIX: Linux, macOS, and Cygwin. */ #include #include #include @@ -46,11 +102,12 @@ #include #include #include +#endif -#define ST_VERSION "1.9.1" +#define ST_VERSION "1.9.2" #define ST_VERSION_MAJOR 1 #define ST_VERSION_MINOR 9 -#define ST_VERSION_PATCH 1 +#define ST_VERSION_PATCH 2 /* Undefine this to remove the context switch callback feature. */ #define ST_SWITCH_CB diff --git a/sched.c b/sched.c index 9734c4d..770ed09 100644 --- a/sched.c +++ b/sched.c @@ -42,8 +42,10 @@ */ #include +#if !defined(WIN64) #include #include +#endif #include #include #include @@ -235,6 +237,10 @@ int st_init(void) void st_destroy(void) { (*_st_eventsys->destroy)(); +#if defined(WIN64) + /* Clean up Winsock, which st_init started. */ + _st_io_destroy(); +#endif } @@ -675,7 +681,20 @@ _st_thread_t *st_thread_create(void *(*start)(void *arg), void *arg, int joinabl if (_st_md_cxt_save(thread->context)) { _st_thread_main(); } - MD_GET_SP(thread) = (long)(stack->sp); + MD_GET_SP(thread) = (intptr_t)(stack->sp); +#ifdef MD_INIT_STACK_BOUNDS + /* Set the stack bounds the OS keeps for the new thread, such as the TIB on Windows. */ + MD_INIT_STACK_BOUNDS(thread, stack->stk_bottom, stack->stk_top); +#endif +#ifdef MD_INIT_THREAD_ENTRY + /* + * Start the new thread in an assembly entry that calls _st_thread_main, not after the save above. + * TODO: Refine the thread entry for all platforms and CPUs, so all start new threads the same way, + * with an assembly entry, and none depends on the save-then-patch-SP trick; only WIN64 has one now. + * See docs/win64_coroutine.md. + */ + MD_INIT_THREAD_ENTRY(thread); +#endif /* If thread is joinable, allocate a termination condition variable */ if (joinable) { diff --git a/st.spec b/st.spec index 662f4c8..92426f5 100644 --- a/st.spec +++ b/st.spec @@ -1,6 +1,6 @@ Summary: State Threads Library Name: st -Version: 1.9.1 +Version: 1.9.2 Release: 1 Copyright: MPL 1.2 or GPL 2+ Packager: Wesley W. Terpstra diff --git a/stk.c b/stk.c index 54d78a1..6595a69 100644 --- a/stk.c +++ b/stk.c @@ -45,9 +45,27 @@ #include #include #include +#if !defined(WIN64) #include +#endif #include "common.h" +/* Protect the red zones with _st_mprotect, which is the system mprotect by default. */ +#if defined(WIN64) +/* Windows has no mprotect: the red zones use VirtualProtect, on VirtualAlloc pages only. */ +#define PROT_NONE 0x0 +#define PROT_READ 0x1 +#define PROT_WRITE 0x2 +static int _st_win64_mprotect(void *addr, size_t len, int prot) +{ + DWORD old; + return VirtualProtect(addr, len, prot ? PAGE_READWRITE : PAGE_NOACCESS, &old) ? 0 : -1; +} +#define _st_mprotect _st_win64_mprotect +#else +#define _st_mprotect mprotect +#endif + /* How much space to leave between the stacks, at each end */ #define REDZONE _st_this_vp.pagesize @@ -94,9 +112,9 @@ _st_stack_t *_st_stack_new(int stack_size) _st_num_free_stacks--; #if defined(DEBUG) && !defined(MD_NO_PROTECT) - mprotect(ts->vaddr, REDZONE, PROT_READ | PROT_WRITE); + _st_mprotect(ts->vaddr, REDZONE, PROT_READ | PROT_WRITE); /* The upper red zone ends the segment; stk_top may be randomized, and extra may have changed since. */ - mprotect(ts->vaddr + ts->vaddr_size - REDZONE, REDZONE, PROT_READ | PROT_WRITE); + _st_mprotect(ts->vaddr + ts->vaddr_size - REDZONE, REDZONE, PROT_READ | PROT_WRITE); #endif _st_delete_stk_segment(ts->vaddr, ts->vaddr_size); @@ -119,8 +137,8 @@ _st_stack_t *_st_stack_new(int stack_size) /* For example, in OpenWRT, the memory at the begin minus 16B by mprotect is read-only. */ #if defined(DEBUG) && !defined(MD_NO_PROTECT) - mprotect(ts->vaddr, REDZONE, PROT_NONE); - mprotect(ts->stk_top + extra, REDZONE, PROT_NONE); + _st_mprotect(ts->vaddr, REDZONE, PROT_NONE); + _st_mprotect(ts->stk_top + extra, REDZONE, PROT_NONE); #endif if (extra) { @@ -152,6 +170,8 @@ static char *_st_new_stk_segment(int size) { #ifdef MALLOC_STACK void *vaddr = malloc(size); +#elif defined(WIN64) + void *vaddr = VirtualAlloc(NULL, size, MEM_RESERVE | MEM_COMMIT, PAGE_READWRITE); #else static int zero_fd = -1; int mmap_flags = MAP_PRIVATE; @@ -183,6 +203,9 @@ void _st_delete_stk_segment(char *vaddr, int size) { #ifdef MALLOC_STACK free(vaddr); +#elif defined(WIN64) + (void) size; + (void) VirtualFree(vaddr, 0, MEM_RELEASE); #else (void) munmap(vaddr, size); #endif diff --git a/tools/backtrace/Makefile b/tools/backtrace/Makefile index 4c9aca3..af249a5 100644 --- a/tools/backtrace/Makefile +++ b/tools/backtrace/Makefile @@ -12,12 +12,32 @@ CPU_ARCHS += $(shell g++ -dM -E - +#endif #include #include #include #include +#if defined(_WIN32) +#include +#include +#include +#pragma comment(lib, "dbghelp.lib") + +/* + * Native Windows has no execinfo.h. backtrace walks the stack with CaptureStackBackTrace, which uses the + * unwind data and the TIB stack bounds that ST sets for each coroutine, and starts at the caller, as glibc. + */ +#define backtrace(buffer, size) ((int)CaptureStackBackTrace(0, (DWORD)(size), (buffer), NULL)) + +/* MSVC gives only the return address of the current function, level 0, which is all bar requires. */ +#define __builtin_return_address(level) ((level) == 0 ? _ReturnAddress() : NULL) + +/* + * backtrace_symbols with DbgHelp: each frame as "address (name+0xoffset)", the glibc form, or the address + * alone with no symbol. One malloc holds the array and the strings, so the caller frees it once. + */ +#define BT_SYMBOL_SIZE 256 +char** backtrace_symbols(void* const* addresses, int nn_addresses) +{ + static int initialized = 0; + HANDLE process = GetCurrentProcess(); + if (!initialized) { + SymInitialize(process, NULL, TRUE); + initialized = 1; + } + + char** symbols = (char**)malloc(nn_addresses * (sizeof(char*) + BT_SYMBOL_SIZE) + 1); + if (!symbols) return NULL; + char* p = (char*)(symbols + nn_addresses); + + char info_buffer[sizeof(SYMBOL_INFO) + BT_SYMBOL_SIZE]; + SYMBOL_INFO* info = (SYMBOL_INFO*)info_buffer; + for (int i = 0; i < nn_addresses; i++, p += BT_SYMBOL_SIZE) { + memset(info_buffer, 0, sizeof(info_buffer)); + info->SizeOfStruct = sizeof(SYMBOL_INFO); + info->MaxNameLen = BT_SYMBOL_SIZE - 1; + DWORD64 offset = 0; + if (SymFromAddr(process, (DWORD64)addresses[i], &offset, info)) { + snprintf(p, BT_SYMBOL_SIZE, "%p (%s+0x%llx)", addresses[i], info->Name, (unsigned long long)offset); + } else { + snprintf(p, BT_SYMBOL_SIZE, "%p", addresses[i]); + } + symbols[i] = p; + } + return symbols; +} +#endif + #ifdef __linux__ #include #include diff --git a/tools/exception/.gitignore b/tools/exception/.gitignore new file mode 100644 index 0000000..38c54e3 --- /dev/null +++ b/tools/exception/.gitignore @@ -0,0 +1 @@ +exception diff --git a/tools/exception/Makefile b/tools/exception/Makefile new file mode 100644 index 0000000..166d69f --- /dev/null +++ b/tools/exception/Makefile @@ -0,0 +1,43 @@ +.PHONY: clean + +LDLIBS=../../obj/libst.a +CXXFLAGS=-g -O0 -I../../obj -I.. -DDEBUG + +OS_NAME = $(shell uname -s) +ST_TARGET = linux-debug +ifeq ($(OS_NAME), Darwin) +ST_TARGET = darwin-debug +CPU_ARCHS = $(shell g++ -dM -E - +#include +#include + +/* Throw a runtime_error whose what() is v. */ +static void throw_value(int v) +{ + throw std::runtime_error(std::to_string(v)); +} + +/* A coroutine's id, and the value it caught, or -1. */ +struct Call { + int id; + int caught; +}; + +/* Throw the id after switches switches, and catch it. */ +static void *switch_then_catch(void *arg) +{ + Call *call = (Call *)arg; + int switches = call->id % 3; + try { + for (int i = 0; i < switches; i++) { + st_usleep(0); + } + throw_value(call->id); + } catch (const std::runtime_error &e) { + call->caught = atoi(e.what()); + } + return NULL; +} + +/* A throw caught in the coroutine that threw it. */ +static int catch_in_coroutine(void) +{ + Call call = {7, -1}; + st_thread_t t; + CHECK((t = st_thread_create(switch_then_catch, &call, 1, 0)) != NULL); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(call.caught == 7); + return 0; +} + +/* + * Unwinding runs the destructor of every frame, deepest first, after the + * deepest frame switched to main and back. + */ +#define FRAMES_DEPTH 32 + +struct Frames { + int destroyed; + int ordered; + int caught; +}; + +class FrameGuard { +public: + FrameGuard(Frames *f, int depth) : f_(f), depth_(depth) { + } + ~FrameGuard() { + if (depth_ != FRAMES_DEPTH - f_->destroyed) { + f_->ordered = 0; + } + f_->destroyed++; + } +private: + Frames *f_; + int depth_; +}; + +static void frames_recurse(Frames *f, int depth) +{ + FrameGuard guard(f, depth); + if (depth < FRAMES_DEPTH) { + frames_recurse(f, depth + 1); + return; + } + st_usleep(0); + throw_value(depth); +} + +static void *frames_coroutine(void *arg) +{ + Frames *f = (Frames *)arg; + try { + frames_recurse(f, 1); + } catch (const std::runtime_error &e) { + f->caught = atoi(e.what()); + } + return NULL; +} + +static int unwind_frames(void) +{ + Frames f = {0, 1, -1}; + st_thread_t t; + CHECK((t = st_thread_create(frames_coroutine, &f, 1, 0)) != NULL); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(f.caught == FRAMES_DEPTH); + CHECK(f.destroyed == FRAMES_DEPTH); + CHECK(f.ordered); + return 0; +} + +/* + * Coroutines switch a different number of times before they throw, so the + * throws interleave, and each catches its own value, on a stack of stk_size. + */ +#define INTERLEAVED 8 + +static int interleaved(int stk_size) +{ + Call calls[INTERLEAVED]; + st_thread_t t[INTERLEAVED]; + for (int i = 0; i < INTERLEAVED; i++) { + calls[i].id = 100 + i; + calls[i].caught = -1; + CHECK((t[i] = st_thread_create(switch_then_catch, &calls[i], 1, stk_size)) != NULL); + } + for (int i = 0; i < INTERLEAVED; i++) { + CHECK(st_thread_join(t[i], NULL) == 0); + CHECK(calls[i].caught == 100 + i); + } + return 0; +} + +/* + * Frames larger than a page, which MSVC probes with __chkstk, down most of a + * 1 MB stack in three coroutines, each switching at the bottom before it + * throws. Each destructor checks that its buffer still holds what it wrote. + */ +#define LARGE_DEPTH 128 +#define LARGE_FRAME 5000 +#define LARGE_STACK (1024 * 1024) +#define LARGE_COROUTINES 3 + +struct Large { + int id; + int destroyed; + int ordered; + int intact; + int caught; +}; + +class LargeGuard { +public: + LargeGuard(Large *l, int depth) : l_(l), depth_(depth) { + buf_[0] = buf_[LARGE_FRAME - 1] = (char)(l->id + depth); + } + ~LargeGuard() { + if (depth_ != LARGE_DEPTH - l_->destroyed) { + l_->ordered = 0; + } + if (buf_[0] != (char)(l_->id + depth_) || buf_[LARGE_FRAME - 1] != buf_[0]) { + l_->intact = 0; + } + l_->destroyed++; + } +private: + Large *l_; + int depth_; + volatile char buf_[LARGE_FRAME]; +}; + +static void large_recurse(Large *l, int depth) +{ + LargeGuard guard(l, depth); + if (depth < LARGE_DEPTH) { + large_recurse(l, depth + 1); + return; + } + st_usleep(0); + throw_value(l->id); +} + +static void *large_coroutine(void *arg) +{ + Large *l = (Large *)arg; + try { + large_recurse(l, 1); + } catch (const std::runtime_error &e) { + l->caught = atoi(e.what()); + } + return NULL; +} + +static int large_frames(void) +{ + Large l[LARGE_COROUTINES]; + st_thread_t t[LARGE_COROUTINES]; + for (int i = 0; i < LARGE_COROUTINES; i++) { + l[i].id = 10 + i; + l[i].destroyed = 0; + l[i].ordered = l[i].intact = 1; + l[i].caught = -1; + CHECK((t[i] = st_thread_create(large_coroutine, &l[i], 1, LARGE_STACK)) != NULL); + } + for (int i = 0; i < LARGE_COROUTINES; i++) { + CHECK(st_thread_join(t[i], NULL) == 0); + CHECK(l[i].caught == 10 + i); + CHECK(l[i].destroyed == LARGE_DEPTH); + CHECK(l[i].ordered && l[i].intact); + } + return 0; +} + +/* + * A destructor switches while its frame is unwound, and another coroutine + * throws and catches before the unwind resumes. + */ +class YieldingGuard { +public: + YieldingGuard(int *destroyed) : destroyed_(destroyed) { + } + ~YieldingGuard() { + st_usleep(0); + st_usleep(0); + (*destroyed_)++; + } +private: + int *destroyed_; +}; + +struct Yielding { + int destroyed; + int caught; +}; + +static void throw_with_yielding_guard(Yielding *y) +{ + YieldingGuard guard(&y->destroyed); + throw_value(300); +} + +static void *yielding_coroutine(void *arg) +{ + Yielding *y = (Yielding *)arg; + try { + throw_with_yielding_guard(y); + } catch (const std::runtime_error &e) { + y->caught = atoi(e.what()); + } + return NULL; +} + +static int yield_while_unwinding(void) +{ + Yielding y = {0, -1}; + Call other = {301, -1}; + st_thread_t t, t2; + CHECK((t = st_thread_create(yielding_coroutine, &y, 1, 0)) != NULL); + CHECK((t2 = st_thread_create(switch_then_catch, &other, 1, 0)) != NULL); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(st_thread_join(t2, NULL) == 0); + CHECK(y.destroyed == 1 && y.caught == 300); + CHECK(other.caught == 301); + return 0; +} + +/* + * One coroutine keeps the exception it caught as an exception_ptr, leaves the + * catch block and switches; another rethrows it and catches the same value. + */ +static std::exception_ptr kept; + +static void *keep_coroutine(void *arg) +{ + (void)arg; + try { + throw_value(400); + } catch (...) { + kept = std::current_exception(); + } + st_usleep(0); + return NULL; +} + +static void *rethrow_coroutine(void *arg) +{ + Call *call = (Call *)arg; + try { + std::rethrow_exception(kept); + } catch (const std::runtime_error &e) { + call->caught = atoi(e.what()); + } + return NULL; +} + +static int exception_ptr_across(void) +{ + Call call = {0, -1}; + st_thread_t t, t2; + CHECK((t = st_thread_create(keep_coroutine, NULL, 1, 0)) != NULL); + st_usleep(0); + CHECK(kept != NULL); + CHECK((t2 = st_thread_create(rethrow_coroutine, &call, 1, 0)) != NULL); + CHECK(st_thread_join(t2, NULL) == 0); + CHECK(st_thread_join(t, NULL) == 0); + CHECK(call.caught == 400); + kept = NULL; + return 0; +} + +/* + * main throws and catches on its own stack while coroutines wait deep in + * their frames, then wakes them and they unwind theirs. + */ +#define PARKED 4 + +static st_cond_t parked_cond; + +static void parked_recurse(Frames *f, int depth) +{ + FrameGuard guard(f, depth); + if (depth < FRAMES_DEPTH) { + parked_recurse(f, depth + 1); + return; + } + st_cond_wait(parked_cond); + throw_value(depth); +} + +static void *parked_coroutine(void *arg) +{ + Frames *f = (Frames *)arg; + try { + parked_recurse(f, 1); + } catch (const std::runtime_error &e) { + f->caught = atoi(e.what()); + } + return NULL; +} + +static int main_while_parked(void) +{ + Frames f[PARKED]; + st_thread_t t[PARKED]; + CHECK((parked_cond = st_cond_new()) != NULL); + for (int i = 0; i < PARKED; i++) { + f[i].destroyed = 0; + f[i].ordered = 1; + f[i].caught = -1; + CHECK((t[i] = st_thread_create(parked_coroutine, &f[i], 1, 0)) != NULL); + } + /* Every coroutine runs down to its st_cond_wait. */ + st_usleep(0); + CHECK(f[0].destroyed == 0); + + int caught = -1; + try { + throw_value(500); + } catch (const std::runtime_error &e) { + caught = atoi(e.what()); + } + CHECK(caught == 500); + + CHECK(st_cond_broadcast(parked_cond) == 0); + for (int i = 0; i < PARKED; i++) { + CHECK(st_thread_join(t[i], NULL) == 0); + CHECK(f[i].caught == FRAMES_DEPTH); + CHECK(f[i].destroyed == FRAMES_DEPTH && f[i].ordered); + } + CHECK(st_cond_destroy(parked_cond) == 0); + return 0; +} + +#if defined(_WIN32) +/* + * SEH itself, which the C++ exceptions above are built on. These functions + * hold no C++ objects, as __try requires. + */ +#define SEH_CODE 0xE0535431u + +static int seh_filter(DWORD code, DWORD want) +{ + return code == want ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH; +} + +/* RaiseException caught by __except, after the __finally between them ran. */ +static DWORD seh_raise(int *finally_ran) +{ + DWORD caught = 0; + __try { + __try { + RaiseException(SEH_CODE, 0, 0, NULL); + } __finally { + (*finally_ran)++; + } + } __except (seh_filter(GetExceptionCode(), SEH_CODE)) { + caught = GetExceptionCode(); + } + return caught; +} + +/* An access violation, a hardware fault, caught by __except. */ +static DWORD seh_fault(void) +{ + int *volatile p = NULL; + DWORD caught = 0; + __try { + *p = 1; + } __except (seh_filter(GetExceptionCode(), EXCEPTION_ACCESS_VIOLATION)) { + caught = GetExceptionCode(); + } + return caught; +} + +struct Seh { + int id; + int finally_ran; + DWORD raised; + DWORD faulted; +}; + +static void *seh_coroutine(void *arg) +{ + Seh *s = (Seh *)arg; + for (int i = 0; i < s->id % 3; i++) { + st_usleep(0); + } + s->raised = seh_raise(&s->finally_ran); + st_usleep(0); + s->faulted = seh_fault(); + return NULL; +} + +/* Coroutines that switch between their SEH exceptions, on the default and a small stack. */ +static int seh(void) +{ + Seh s[INTERLEAVED]; + st_thread_t t[INTERLEAVED]; + for (int i = 0; i < INTERLEAVED; i++) { + memset(&s[i], 0, sizeof(s[i])); + s[i].id = i; + CHECK((t[i] = st_thread_create(seh_coroutine, &s[i], 1, i % 2 ? 32 * 1024 : 0)) != NULL); + } + for (int i = 0; i < INTERLEAVED; i++) { + CHECK(st_thread_join(t[i], NULL) == 0); + CHECK(s[i].raised == SEH_CODE && s[i].finally_ran == 1); + CHECK(s[i].faulted == EXCEPTION_ACCESS_VIOLATION); + } + return 0; +} +#endif + +int main(int argc, char **argv) +{ + (void)argc; + (void)argv; + CHECK(tool_init() == 0); + + CHECK(catch_in_coroutine() == 0); + CHECK(unwind_frames() == 0); + CHECK(interleaved(0) == 0); + CHECK(interleaved(32 * 1024) == 0); + CHECK(large_frames() == 0); + CHECK(yield_while_unwinding() == 0); + CHECK(exception_ptr_across() == 0); + CHECK(main_while_parked() == 0); +#if defined(_WIN32) + CHECK(seh() == 0); +#endif + + printf("exception OK\n"); + return 0; +} diff --git a/tools/helloworld/Makefile b/tools/helloworld/Makefile index b4643c6..9c810ea 100644 --- a/tools/helloworld/Makefile +++ b/tools/helloworld/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - 2); - /* The keys made so far are key and plain. */ - int keys[limit]; + /* The keys made so far are key and plain. On the heap, because MSVC has no variable length arrays. */ + int *keys = (int *)malloc(limit * sizeof(int)); + CHECK(keys); keys[0] = key; keys[1] = plain; for (int i = 2; i < limit; i++) { @@ -332,6 +333,7 @@ static int limit(void) CHECK(st_thread_getspecific(keys[i]) == &keys[i]); CHECK(st_thread_setspecific(keys[i], NULL) == 0); } + free(keys); return 0; } diff --git a/tools/lifecycle/Makefile b/tools/lifecycle/Makefile index d015d40..6e830b9 100644 --- a/tools/lifecycle/Makefile +++ b/tools/lifecycle/Makefile @@ -12,12 +12,32 @@ CPU_ARCHS += $(shell g++ -dM -E - #include #include +#endif #include "tool.h" @@ -66,7 +68,13 @@ static int set_primordial_stack(void) { char *top = NULL; size_t size = 0; -#ifdef __APPLE__ +#if defined(_WIN32) + /* The whole reserved stack, from the TIB's DeallocationStack up to its StackBase. */ + ULONG_PTR low = 0, high = 0; + GetCurrentThreadStackLimits(&low, &high); + top = (char *)high; + size = (size_t)(high - low); +#elif defined(__APPLE__) pthread_t self = pthread_self(); top = (char *)pthread_get_stackaddr_np(self); size = pthread_get_stacksize_np(self); @@ -83,6 +91,8 @@ static int set_primordial_stack(void) return 0; } +/* Windows has no rlimit, so it does not support this check. */ +#ifndef _WIN32 /* The descriptor limit _st_io_init sets from the limit before st_init. */ static int check_fdlimit(int eventsys, const struct rlimit *before) { @@ -111,6 +121,7 @@ static int check_fdlimit(int eventsys, const struct rlimit *before) (unsigned long long)before->rlim_cur, (unsigned long long)before->rlim_max); return 0; } +#endif /* Block four coroutines, then interrupt and join them; each must fail with EINTR at once. */ static int interrupt_all(void) @@ -182,8 +193,10 @@ int main(int argc, char **argv) CHECK(set_primordial_stack() == 0); +#ifndef _WIN32 struct rlimit before; CHECK(getrlimit(RLIMIT_NOFILE, &before) == 0); +#endif CHECK(st_init() == 0); printf("ST: init ok, eventsys=%s\n", st_get_eventsys_name()); @@ -198,7 +211,10 @@ int main(int argc, char **argv) CHECK(st_init() == 0); CHECK(st_get_eventsys() == eventsys); + /* Windows has no rlimit, so it does not support this check. */ +#ifndef _WIN32 CHECK(check_fdlimit(eventsys, &before) == 0); +#endif CHECK(interrupt_all() == 0); st_destroy(); diff --git a/tools/pipe/Makefile b/tools/pipe/Makefile index 9fc148f..51913a5 100644 --- a/tools/pipe/Makefile +++ b/tools/pipe/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - #include #include @@ -392,3 +407,4 @@ int main(int argc, char **argv) printf("pipe OK\n"); return 0; } +#endif /* _WIN32 */ diff --git a/tools/poll/Makefile b/tools/poll/Makefile index b32514c..f15ae21 100644 --- a/tools/poll/Makefile +++ b/tools/poll/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - #include +#endif #include /* Long enough that a coroutine still blocked here is a failure, not a slow run. */ @@ -33,7 +36,7 @@ static struct channel channels[NB_CHANNELS]; static int open_pipe(struct channel *c) { int fds[2]; - CHECK(pipe(fds) == 0); + CHECK(tool_pipe(fds) == 0); CHECK((c->rd = st_netfd_open(fds[0])) != NULL); CHECK((c->wr = st_netfd_open(fds[1])) != NULL); return 0; @@ -42,7 +45,7 @@ static int open_pipe(struct channel *c) static int open_socketpair(struct channel *c) { int fds[2]; - CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0); + CHECK(tool_socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0); CHECK((c->rd = st_netfd_open_socket(fds[0])) != NULL); CHECK((c->wr = st_netfd_open_socket(fds[1])) != NULL); return 0; @@ -390,12 +393,15 @@ static int netfd_poll(void) struct channel c; CHECK(open_pipe(&c) == 0); int wfd = st_netfd_fileno(c.wr); +#ifndef _WIN32 + /* Windows does not support this check: it cannot read back whether a socket is non-blocking. */ CHECK(fcntl(wfd, F_GETFL) & O_NONBLOCK); +#endif char buf[4096]; memset(buf, 'f', sizeof(buf)); int filled = 0; for (;;) { - ssize_t n = write(wfd, buf, sizeof(buf)); + ssize_t n = tool_write(wfd, buf, sizeof(buf)); if (n < 0) { CHECK(errno == EAGAIN || errno == EWOULDBLOCK); break; diff --git a/tools/porting/Makefile b/tools/porting/Makefile index 11e3a49..0a63d72 100644 --- a/tools/porting/Makefile +++ b/tools/porting/Makefile @@ -11,9 +11,26 @@ CPU_ARCHS += $(shell g++ -dM -E - #include +#ifndef _WIN32 #include +#endif #include void bar() @@ -30,6 +32,9 @@ int main(int argc, char** argv) #ifdef _WIN32 printf("_WIN32: %d\n", _WIN32); #endif +#ifdef _WIN64 + printf("_WIN64: %d\n", _WIN64); +#endif printf("\nCPU specs:\n"); #ifdef __mips__ @@ -54,11 +59,20 @@ int main(int argc, char** argv) #ifdef __aarch64__ printf("__aarch64__: %d\n", __aarch64__); #endif +#ifdef _M_X64 + printf("_M_X64: %d\n", _M_X64); +#endif +#ifdef _M_ARM64 + printf("_M_ARM64: %d\n", _M_ARM64); +#endif printf("\nCompiler specs:\n"); #ifdef __GLIBC__ printf("__GLIBC__: %d\n", __GLIBC__); #endif +#ifdef _MSC_VER + printf("_MSC_VER: %d\n", _MSC_VER); +#endif printf("\nCalling conventions:\n"); foo(); @@ -89,6 +103,10 @@ int main(int argc, char** argv) #if defined(__x86_64__) supported = 1; #endif +#elif defined(_WIN32) +#if defined(_M_X64) + supported = 1; +#endif #endif if (!supported) { printf("\nUnsupported OS or CPU, port md.h and an md_*.S file first\n"); diff --git a/tools/stress/Makefile b/tools/stress/Makefile index faeb6d6..d89c3ac 100644 --- a/tools/stress/Makefile +++ b/tools/stress/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - #include #include +#endif /* Long enough that a coroutine still blocked here is a failure, not a slow run. */ #define BLOCK_US (5 * 1000 * 1000) @@ -405,12 +408,15 @@ int main(int argc, char **argv) * limit for a large count before st_init, which reads it. */ int need = 2 * count + 32; +#ifndef _WIN32 + /* Windows does not support this: it has no rlimit, and ST allows 2^24 sockets there. */ struct rlimit rl; CHECK(getrlimit(RLIMIT_NOFILE, &rl) == 0); if (rl.rlim_cur < (rlim_t)need) { rl.rlim_cur = need; setrlimit(RLIMIT_NOFILE, &rl); } +#endif CHECK(tool_init() == 0); if (st_getfdlimit() < need) { diff --git a/tools/sync/Makefile b/tools/sync/Makefile index ccbaa6d..830c25d 100644 --- a/tools/sync/Makefile +++ b/tools/sync/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - #include #include +#endif /* Long enough that a coroutine still blocked here is a failure, not a slow run. */ #define BLOCK_US (5 * 1000 * 1000) @@ -447,6 +449,7 @@ static int pair(int family, st_netfd_t *c, st_netfd_t *s) return 0; } +#ifndef _WIN32 /* Read exactly n bytes, and then nothing more is there. */ static int drain(st_netfd_t fd, size_t n) { @@ -462,6 +465,7 @@ static int drain(st_netfd_t fd, size_t n) CHECK(more == -1 && err == ETIME); return 0; } +#endif /* The residual counts: short reads at EOF, and partial writes on a timeout. */ static int resid(int family) @@ -496,7 +500,12 @@ static int resid(int family) CHECK(!memcmp(buf, "0123456789", 10)); CHECK(st_netfd_close(s) == 0); - /* The peer does not read, so a big write times out part way. */ +#ifndef _WIN32 + /* + * The peer does not read, so a big write times out part way. Not on native + * Windows: Winsock never sends part of a non-blocking send, it takes the whole + * buffer or fails with WSAEWOULDBLOCK, so a write never ends part way. + */ size_t big = 1024 * 1024; char *data = calloc(1, big); CHECK(data != NULL); @@ -536,6 +545,7 @@ static int resid(int family) free(data); CHECK(st_netfd_close(c) == 0); CHECK(st_netfd_close(s) == 0); +#endif return 0; } @@ -564,7 +574,7 @@ static int timeouts(int family) for (; n < 16; n++) { struct sockaddr_storage addr; socklen_t len = tool_loopback(family, port, &addr); - int fd = socket(family, SOCK_STREAM, 0); + int fd = (int)socket(family, SOCK_STREAM, 0); CHECK(fd >= 0); CHECK((clients[n] = st_netfd_open_socket(fd)) != NULL); errno = 0; @@ -593,9 +603,24 @@ static void dtor(void *v) dtor_last = v; } +/* + * Whether fd is open; if not, errno is EBADF. On native Windows a socket is + * not a file descriptor, so getsockopt asks for its type, and a closed socket + * fails with WSAENOTSOCK, the Winsock form of EBADF. + */ static int is_open(int fd) { +#if defined(_WIN32) + int type; + int len = sizeof(type); + if (getsockopt((SOCKET)fd, SOL_SOCKET, SO_TYPE, (char *)&type, &len) == 0) { + return 1; + } + errno = (WSAGetLastError() == WSAENOTSOCK) ? EBADF : EINVAL; + return 0; +#else return fcntl(fd, F_GETFD) != -1; +#endif } /* The netfd specific data, and st_netfd_free against st_netfd_close. */ diff --git a/tools/thread/Makefile b/tools/thread/Makefile index 999611c..cd236e2 100644 --- a/tools/thread/Makefile +++ b/tools/thread/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - +#ifdef _WIN32 +#include +#else #include +#endif /* Long enough that a sleep still blocked here is a failure, not a slow run. */ #define BLOCK_US (5 * 1000 * 1000) @@ -21,9 +26,18 @@ /* The real monotonic clock, independent of ST. */ static st_utime_t real_us(void) { +#ifdef _WIN32 + /* Windows has no clock_gettime; the performance counter is its monotonic clock. */ + LARGE_INTEGER freq, now; + QueryPerformanceFrequency(&freq); + QueryPerformanceCounter(&now); + return (st_utime_t)(now.QuadPart / freq.QuadPart) * 1000000LL + + (st_utime_t)(now.QuadPart % freq.QuadPart) * 1000000LL / freq.QuadPart; +#else struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return (st_utime_t)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000; +#endif } /* @@ -90,6 +104,14 @@ static int custom_clock(void) static int fork_custom_clock(void) { fflush(stdout); +#ifdef _WIN32 + /* Run this program again as the child, with the same environment, and wait for its exit code. */ + char path[MAX_PATH]; + DWORD n = GetModuleFileNameA(NULL, path, sizeof(path)); + CHECK(n > 0 && n < sizeof(path)); + CHECK(_spawnl(_P_WAIT, path, "time", "custom-clock", NULL) == 0); + return 0; +#else pid_t pid = fork(); CHECK(pid >= 0); if (pid == 0) { @@ -102,6 +124,7 @@ static int fork_custom_clock(void) CHECK(waitpid(pid, &status, 0) == pid); CHECK(WIFEXITED(status) && WEXITSTATUS(status) == 0); return 0; +#endif } /* The clock never goes back, and moves across a sleep. */ @@ -238,6 +261,14 @@ static int timecache(void) int main(int argc, char **argv) { +#ifdef _WIN32 + /* The child of fork_custom_clock. */ + if (argc == 2 && !strcmp(argv[1], "custom-clock")) { + int r = custom_clock(); + fflush(stdout); + return r; + } +#endif CHECK(fork_custom_clock() == 0); CHECK(tool_init() == 0); diff --git a/tools/tool.h b/tools/tool.h index 3ec45f3..eb26b07 100644 --- a/tools/tool.h +++ b/tools/tool.h @@ -13,12 +13,18 @@ #include #include #include +#ifndef _WIN32 #include #include #include #include +#endif +/* On native Windows, st.h brings the Winsock 2 headers. */ #include +#if defined(_WIN32) +#include +#endif /* Print the failing check and its line, then return 1 from the caller. */ #define CHECK(cond) do { \ @@ -34,6 +40,9 @@ */ static inline int tool_eventsys(void) { +#ifdef _MSC_VER +#pragma warning(suppress: 4996) /* getenv is not deprecated for this use. */ +#endif const char *v = getenv("ST_TOOL_EVENTSYS"); if (!v || !strcmp(v, "select")) { return ST_EVENTSYS_SELECT; @@ -47,6 +56,11 @@ static inline int tool_eventsys(void) /* The name st_get_eventsys_name() reports for an event system. */ static inline const char *tool_eventsys_name(int eventsys) { +#if defined(_WIN32) + /* Both event systems are WSAPoll on native Windows. */ + (void)eventsys; + return "wsapoll"; +#else if (eventsys == ST_EVENTSYS_SELECT) { return "select"; } @@ -55,6 +69,7 @@ static inline const char *tool_eventsys_name(int eventsys) #else return "epoll"; #endif +#endif } /* @@ -72,6 +87,16 @@ static inline int tool_init(void) return 0; } +/* Close a socket that no st_netfd_t owns: closesocket on native Windows, close elsewhere. */ +static inline void tool_close_socket(int fd) +{ +#if defined(_WIN32) + closesocket(fd); +#else + close(fd); +#endif +} + /* Fill addr with the loopback address of family, 127.0.0.1 or ::1, and port. */ static inline socklen_t tool_loopback(int family, int port, struct sockaddr_storage *addr) { @@ -79,14 +104,14 @@ static inline socklen_t tool_loopback(int family, int port, struct sockaddr_stor if (family == AF_INET6) { struct sockaddr_in6 *a = (struct sockaddr_in6 *)addr; a->sin6_family = AF_INET6; - a->sin6_port = htons(port); + a->sin6_port = htons((unsigned short)port); a->sin6_addr = in6addr_loopback; return sizeof(*a); } struct sockaddr_in *a = (struct sockaddr_in *)addr; a->sin_family = AF_INET; - a->sin_port = htons(port); + a->sin_port = htons((unsigned short)port); a->sin_addr.s_addr = htonl(INADDR_LOOPBACK); return sizeof(*a); } @@ -101,7 +126,7 @@ static inline st_netfd_t tool_listen(int family, int backlog, int *port) struct sockaddr_storage addr; socklen_t len = tool_loopback(family, 0, &addr); - int fd = socket(family, SOCK_STREAM, 0); + int fd = (int)socket(family, SOCK_STREAM, 0); if (fd < 0) { return NULL; } @@ -109,7 +134,7 @@ static inline st_netfd_t tool_listen(int family, int backlog, int *port) if (bind(fd, (struct sockaddr *)&addr, len) < 0 || listen(fd, backlog) < 0 || getsockname(fd, (struct sockaddr *)&addr, &len) < 0) { int err = errno; - close(fd); + tool_close_socket(fd); errno = err; return NULL; } @@ -117,7 +142,7 @@ static inline st_netfd_t tool_listen(int family, int backlog, int *port) st_netfd_t stfd = st_netfd_open_socket(fd); if (!stfd) { int err = errno; - close(fd); + tool_close_socket(fd); errno = err; return NULL; } @@ -139,7 +164,7 @@ static inline st_netfd_t tool_connect(int family, int port, st_utime_t timeout) struct sockaddr_storage addr; socklen_t len = tool_loopback(family, port, &addr); - int fd = socket(family, SOCK_STREAM, 0); + int fd = (int)socket(family, SOCK_STREAM, 0); if (fd < 0) { return NULL; } @@ -147,7 +172,7 @@ static inline st_netfd_t tool_connect(int family, int port, st_utime_t timeout) st_netfd_t stfd = st_netfd_open_socket(fd); if (!stfd) { int err = errno; - close(fd); + tool_close_socket(fd); errno = err; return NULL; } @@ -161,4 +186,143 @@ static inline st_netfd_t tool_connect(int family, int port, st_utime_t timeout) return stfd; } +#if defined(_WIN32) +/* + * A connected pair of stream sockets of family, AF_INET over 127.0.0.1 or + * AF_UNIX at a path in the temp folder, made with bind, listen, connect and + * accept. fds[0] is the accepted end and fds[1] the client. Returns -1 with + * errno set on failure. + */ +static inline int tool_stream_pair(int family, int fds[2]) +{ + struct sockaddr_storage addr; + int len; + if (family == AF_UNIX) { + static int seq = 0; + char dir[MAX_PATH]; + struct sockaddr_un *a = (struct sockaddr_un *)&addr; + memset(&addr, 0, sizeof(addr)); + a->sun_family = AF_UNIX; + if (!GetTempPathA(sizeof(dir), dir)) { + errno = ENOENT; + return -1; + } + snprintf(a->sun_path, sizeof(a->sun_path), "%sst-tool-%lu-%d.sock", dir, GetCurrentProcessId(), seq++); + DeleteFileA(a->sun_path); + len = (int)sizeof(*a); + } else { + len = (int)tool_loopback(AF_INET, 0, &addr); + } + + SOCKET l = INVALID_SOCKET, c = INVALID_SOCKET, s = INVALID_SOCKET; + if ((l = socket(family, SOCK_STREAM, 0)) == INVALID_SOCKET + || bind(l, (struct sockaddr *)&addr, len) || listen(l, 1) + || getsockname(l, (struct sockaddr *)&addr, &len) + || (c = socket(family, SOCK_STREAM, 0)) == INVALID_SOCKET + || connect(c, (struct sockaddr *)&addr, len) + || (s = accept(l, NULL, NULL)) == INVALID_SOCKET) { + if (c != INVALID_SOCKET) { + closesocket(c); + } + s = INVALID_SOCKET; + } + if (l != INVALID_SOCKET) { + closesocket(l); + } + if (family == AF_UNIX) { + DeleteFileA(((struct sockaddr_un *)&addr)->sun_path); + } + if (s == INVALID_SOCKET) { + errno = EIO; + return -1; + } + + fds[0] = (int)s; + fds[1] = (int)c; + return 0; +} +#endif + +/* + * A one-way channel, fds[0] to read and fds[1] to write: pipe(2), or on + * native Windows, where ST takes sockets only, a loopback TCP pair (D26). + */ +static inline int tool_pipe(int fds[2]) +{ +#if defined(_WIN32) + return tool_stream_pair(AF_INET, fds); +#else + return pipe(fds); +#endif +} + +#if defined(_WIN32) +/* + * A pair of UDP sockets on 127.0.0.1, each connected to the other, so send + * and recv carry datagrams between them. Returns -1 with errno set on failure. + */ +static inline int tool_dgram_pair(int fds[2]) +{ + struct sockaddr_storage a0, a1; + int l0 = (int)tool_loopback(AF_INET, 0, &a0), l1 = (int)tool_loopback(AF_INET, 0, &a1); + SOCKET s0 = socket(AF_INET, SOCK_DGRAM, 0), s1 = socket(AF_INET, SOCK_DGRAM, 0); + if (s0 == INVALID_SOCKET || s1 == INVALID_SOCKET + || bind(s0, (struct sockaddr *)&a0, l0) || bind(s1, (struct sockaddr *)&a1, l1) + || getsockname(s0, (struct sockaddr *)&a0, &l0) || getsockname(s1, (struct sockaddr *)&a1, &l1) + || connect(s0, (struct sockaddr *)&a1, l1) || connect(s1, (struct sockaddr *)&a0, l0)) { + if (s0 != INVALID_SOCKET) { + closesocket(s0); + } + if (s1 != INVALID_SOCKET) { + closesocket(s1); + } + errno = EIO; + return -1; + } + + fds[0] = (int)s0; + fds[1] = (int)s1; + return 0; +} +#endif + +/* + * socketpair(2). Native Windows has none, and its AF_UNIX is stream only, so + * there an AF_UNIX stream pair is connected through a path in the temp folder + * (D26), and a datagram pair is a connected loopback UDP pair (D28). + */ +static inline int tool_socketpair(int domain, int type, int protocol, int fds[2]) +{ +#if defined(_WIN32) + if (domain != AF_UNIX || (type != SOCK_STREAM && type != SOCK_DGRAM) || protocol != 0) { + errno = EINVAL; + return -1; + } + if (type == SOCK_DGRAM) { + return tool_dgram_pair(fds); + } + return tool_stream_pair(AF_UNIX, fds); +#else + return socketpair(domain, type, protocol, fds); +#endif +} + +/* + * write(2) to a descriptor from tool_pipe or tool_socketpair. On native + * Windows it is a socket: send, with EAGAIN when it would block. + */ +static inline ssize_t tool_write(int fd, const void *buf, size_t n) +{ +#if defined(_WIN32) + int r = send((SOCKET)fd, (const char *)buf, (int)n, 0); + if (r == SOCKET_ERROR) { + errno = (WSAGetLastError() == WSAEWOULDBLOCK) ? EAGAIN : EIO; + return -1; + } + return r; +#else + return write(fd, buf, n); +#endif +} + #endif /* ST_TOOL_H */ diff --git a/tools/udp/Makefile b/tools/udp/Makefile index d6dcc56..2986733 100644 --- a/tools/udp/Makefile +++ b/tools/udp/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - +#endif /* Long enough that a coroutine still blocked here is a failure, not a slow run. */ #define BLOCK_US (5 * 1000 * 1000) @@ -35,14 +37,14 @@ static st_netfd_t udp_bind(int family, struct sockaddr_storage *addr, int *addrlen) { socklen_t len = tool_loopback(family, 0, addr); - int fd = socket(family, SOCK_DGRAM, 0); + int fd = (int)socket(family, SOCK_DGRAM, 0); if (fd < 0) { return NULL; } if (bind(fd, (struct sockaddr *)addr, len) < 0 || getsockname(fd, (struct sockaddr *)addr, &len) < 0) { int err = errno; - close(fd); + tool_close_socket(fd); errno = err; return NULL; } @@ -50,7 +52,7 @@ static st_netfd_t udp_bind(int family, struct sockaddr_storage *addr, int *addrl st_netfd_t stfd = st_netfd_open_socket(fd); if (!stfd) { int err = errno; - close(fd); + tool_close_socket(fd); errno = err; return NULL; } diff --git a/tools/unix/Makefile b/tools/unix/Makefile index d883d5c..10b7bd0 100644 --- a/tools/unix/Makefile +++ b/tools/unix/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - +/* On native Windows, st.h brings struct iovec and tool.h struct sockaddr_un from afunix.h. */ +#ifndef _WIN32 #include #include +#else +#include +/* The POSIX names of the MSVC CRT calls, for the socket paths. */ +#define access _access +#define unlink _unlink +#define F_OK 0 +#endif /* Long enough that a coroutine still blocked here is a failure, not a slow run. */ #define BLOCK_US (5 * 1000 * 1000) @@ -21,6 +30,24 @@ /* A timeout that is expected to expire. */ #define SHORT_US (5 * 1000) +#ifdef _WIN32 +/* + * Windows has no /tmp, so the stream paths are in the temp folder, set by + * temp_paths before use. Its AF_UNIX is stream only, with no datagram paths. + */ +static char stream_path[MAX_PATH], missing_path[MAX_PATH]; +#define STREAM_PATH stream_path +#define MISSING_PATH missing_path + +static int temp_paths(void) +{ + char dir[MAX_PATH]; + CHECK(GetTempPathA(sizeof(dir), dir) != 0); + snprintf(stream_path, sizeof(stream_path), "%sst-tool-unix.sock", dir); + snprintf(missing_path, sizeof(missing_path), "%sst-tool-unix-missing.sock", dir); + return 0; +} +#else /* The paths, removed by these literal paths before and after use. */ #define STREAM_PATH "/tmp/st-tool-unix.sock" #define DGRAM_PATH "/tmp/st-tool-unix-dgram.sock" @@ -32,6 +59,7 @@ static const char *dgram_client_paths[DGRAM_CLIENTS] = { "/tmp/st-tool-unix-dgram-2.sock", "/tmp/st-tool-unix-dgram-3.sock", }; +#endif /* The stream echo: clients, messages per client, and the largest payload. */ #define CLIENTS 8 @@ -49,12 +77,14 @@ static const char *dgram_client_paths[DGRAM_CLIENTS] = { static void remove_paths(void) { unlink(STREAM_PATH); - unlink(DGRAM_PATH); unlink(MISSING_PATH); +#ifndef _WIN32 + unlink(DGRAM_PATH); unlink("/tmp/st-tool-unix-dgram-0.sock"); unlink("/tmp/st-tool-unix-dgram-1.sock"); unlink("/tmp/st-tool-unix-dgram-2.sock"); unlink("/tmp/st-tool-unix-dgram-3.sock"); +#endif } static socklen_t unix_addr(const char *path, struct sockaddr_un *addr) @@ -70,7 +100,7 @@ static st_netfd_t unix_bind(int type, const char *path) { struct sockaddr_un addr; socklen_t len = unix_addr(path, &addr); - int fd = socket(AF_UNIX, type, 0); + int fd = (int)socket(AF_UNIX, type, 0); if (fd < 0) { return NULL; } @@ -84,7 +114,7 @@ static st_netfd_t unix_bind(int type, const char *path) if ((type == SOCK_DGRAM && setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &rcvbuf, sizeof(rcvbuf)) < 0) || bind(fd, (struct sockaddr *)&addr, len) < 0 || (type == SOCK_STREAM && listen(fd, CLIENTS) < 0)) { int err = errno; - close(fd); + tool_close_socket(fd); errno = err; return NULL; } @@ -92,7 +122,7 @@ static st_netfd_t unix_bind(int type, const char *path) st_netfd_t stfd = st_netfd_open_socket(fd); if (!stfd) { int err = errno; - close(fd); + tool_close_socket(fd); errno = err; } return stfd; @@ -103,7 +133,7 @@ static st_netfd_t unix_connect(const char *path) { struct sockaddr_un addr; socklen_t len = unix_addr(path, &addr); - int fd = socket(AF_UNIX, SOCK_STREAM, 0); + int fd = (int)socket(AF_UNIX, SOCK_STREAM, 0); if (fd < 0) { return NULL; } @@ -111,7 +141,7 @@ static st_netfd_t unix_connect(const char *path) st_netfd_t stfd = st_netfd_open_socket(fd); if (!stfd) { int err = errno; - close(fd); + tool_close_socket(fd); errno = err; return NULL; } @@ -416,8 +446,11 @@ static int stream_echo(void) /* A path with no socket, and a path whose listener is gone. */ st_netfd_t fd; unlink(MISSING_PATH); +#ifndef _WIN32 + /* Windows does not support this check: a connect to a missing path fails with ECONNREFUSED there. */ errno = 0; CHECK(unix_connect(MISSING_PATH) == NULL && errno == ENOENT); +#endif CHECK((fd = unix_bind(SOCK_STREAM, MISSING_PATH)) != NULL); CHECK(st_netfd_close(fd) == 0); errno = 0; @@ -426,6 +459,8 @@ static int stream_echo(void) return 0; } +#ifndef _WIN32 +/* Windows does not support the datagram echo: its AF_UNIX is stream only, with no datagram paths. */ static struct { st_netfd_t server; int server_msgs; @@ -584,6 +619,7 @@ static int dgram_echo(void) CHECK(unlink(DGRAM_PATH) == 0); return 0; } +#endif /* The socketpair bulk transfer: the size and the largest write. */ #define PAIR_BYTES (512 * 1024) @@ -621,7 +657,7 @@ static int pair_stream(void) { int sv[2]; st_netfd_t a, b; - CHECK(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); + CHECK(tool_socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); CHECK((a = st_netfd_open_socket(sv[0])) != NULL); CHECK((b = st_netfd_open_socket(sv[1])) != NULL); CHECK(st_netfd_fileno(a) == sv[0] && st_netfd_fileno(b) == sv[1]); @@ -676,12 +712,15 @@ static void *pair_read_forever(void *arg) return NULL; } -/* A datagram socketpair: st_write and st_read keep the boundaries. */ +/* + * A datagram socketpair: st_write and st_read keep the boundaries. On native + * Windows it is a connected loopback UDP pair (D28). + */ static int pair_dgram(void) { int sv[2]; st_netfd_t a, b; - CHECK(socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) == 0); + CHECK(tool_socketpair(AF_UNIX, SOCK_DGRAM, 0, sv) == 0); CHECK((a = st_netfd_open_socket(sv[0])) != NULL); CHECK((b = st_netfd_open_socket(sv[1])) != NULL); @@ -711,7 +750,10 @@ static int pair_dgram(void) static int run(void) { CHECK(stream_echo() == 0); +#ifndef _WIN32 + /* Windows does not support it: its AF_UNIX is stream only. */ CHECK(dgram_echo() == 0); +#endif CHECK(pair_stream() == 0); CHECK(pair_dgram() == 0); return 0; @@ -721,6 +763,9 @@ int main(int argc, char **argv) { CHECK(tool_init() == 0); +#ifdef _WIN32 + CHECK(temp_paths() == 0); +#endif remove_paths(); int r = run(); remove_paths(); diff --git a/tools/verify/Makefile b/tools/verify/Makefile index e5acc4d..de59b30 100644 --- a/tools/verify/Makefile +++ b/tools/verify/Makefile @@ -12,12 +12,31 @@ CPU_ARCHS += $(shell g++ -dM -E - #include #include @@ -37,7 +38,7 @@ void* start(void* arg) r0 = st_mutex_unlock(lock); printf("ST: thread unlock\n"); - return (void*)(long)r0; + return (void*)(intptr_t)r0; } int main(int argc, char** argv) diff --git a/utest/Makefile b/utest/Makefile index 955157e..27569f9 100644 --- a/utest/Makefile +++ b/utest/Makefile @@ -6,14 +6,36 @@ ST_UTEST = . # The main dir of gtest. GTEST_DIR = $(ST_UTEST)/gtest-fit/googletest +# The output flags of the compiler, archiver and linker, the executable suffix, and the libraries. WIN64 is native +# Windows x64 with MSVC (cl, lib), as in ../Makefile; NOMINMAX keeps windows.h from defining min and max. +ifeq ($(OS), WIN64) +CXX = cl +AR = lib +ARFLAGS = -nologo +CXX_OUT = -Fo +AR_OUT = -OUT: +LINK_OUT = -Fe +EXE = .exe +LIBS = ws2_32.lib +CXXFLAGS += -nologo -Z7 -Od -EHsc -DNOMINMAX +CXXFLAGS += -DGTEST_USE_OWN_TR1_TUPLE=1 +WARNFLAGS += -W4 +else +CXX_OUT = -o $(EMPTY) +AR_OUT = +LINK_OUT = -o $(EMPTY) +EXE = +LIBS = -lpthread -ldl + # Flags passed to the C++ compiler. CXXFLAGS += -g -O0 -std=c++11 CXXFLAGS += -DGTEST_USE_OWN_TR1_TUPLE=1 # Flags for warnings. WARNFLAGS += -Wall -Wno-deprecated-declarations -Wno-unused-private-field -Wno-unused-command-line-argument +endif # House-keeping build targets. -all : $(ST_DIR)/obj/st_utest +all : $(ST_DIR)/obj/st_utest$(EXE) # Auto-setup: symlink gtest-fit from SRS 3rdparty if not present. $(GTEST_DIR)/include/gtest/gtest.h: @@ -39,13 +61,13 @@ GTEST_SRCS_ = $(GTEST_DIR)/src/*.cc $(GTEST_DIR)/src/*.h $(GTEST_DIR)/include/gt # conservative and not optimized. This is fine as Google Test # compiles fast and for ordinary users its source rarely changes. $(ST_DIR)/obj/gtest-all.o : $(GTEST_DIR)/include/gtest/gtest.h $(GTEST_SRCS_) - $(CXX) -c $(GTEST_DIR)/src/gtest-all.cc -o $@ \ + $(CXX) -c $(GTEST_DIR)/src/gtest-all.cc $(CXX_OUT)$@ \ $(CXXFLAGS) $(UTEST_FLAGS) \ $(WARNFLAGS) \ -I$(GTEST_DIR)/include -I$(GTEST_DIR) $(ST_DIR)/obj/gtest.a : $(ST_DIR)/obj/gtest-all.o - $(AR) $(ARFLAGS) $@ $^ + $(AR) $(ARFLAGS) $(AR_OUT)$@ $^ ##################################################################################### ##################################################################################### @@ -74,12 +96,12 @@ OBJECTS = $(addprefix $(ST_DIR)/obj/,$(OBJECTS_FILES)) # Objects, build each object of utest $(ST_DIR)/obj/%.o : %.cpp $(ST_UTEST_DEPS) $(UTEST_DEPS) $(GTEST_DIR)/include/gtest/gtest.h - $(CXX) -c $< -o $@ \ + $(CXX) -c $< $(CXX_OUT)$@ \ $(CXXFLAGS) $(UTEST_FLAGS) \ $(WARNFLAGS) \ -I$(GTEST_DIR)/include -I$(ST_UTEST) -I$(ST_DIR) -I$(ST_DIR)/obj # Generate the utest binary -$(ST_DIR)/obj/st_utest : $(OBJECTS) $(ST_DIR)/obj/gtest.a $(ST_UTEST_DEPS) - $(CXX) -o $@ $(CXXFLAGS) $(UTEST_FLAGS) \ - $^ -lpthread -ldl +$(ST_DIR)/obj/st_utest$(EXE) : $(OBJECTS) $(ST_DIR)/obj/gtest.a $(ST_UTEST_DEPS) + $(CXX) $(LINK_OUT)$@ $(CXXFLAGS) $(UTEST_FLAGS) \ + $^ $(LIBS) diff --git a/utest/st_utest.hpp b/utest/st_utest.hpp index 03f4810..72e187e 100644 --- a/utest/st_utest.hpp +++ b/utest/st_utest.hpp @@ -15,15 +15,133 @@ #include #include +#ifndef _WIN32 #include #include +#endif +#ifdef _WIN32 +// winnt.h defines VOID as void, which the tests use as an empty prefix. +#undef VOID +// For the tests that declare ST's thread-local variables, as md.h maps it for WIN64. +#define __thread __declspec(thread) +// MSVC has no frame address builtin; the slot of the return address is in the caller's frame, next to it. +#include +#define __builtin_frame_address(level) _AddressOfReturnAddress() +static inline int getpagesize() +{ + SYSTEM_INFO si; + GetSystemInfo(&si); + return (int)si.dwPageSize; +} +// Sleeps the OS thread, rounded up to milliseconds. +static inline int usleep(unsigned int us) +{ + Sleep((us + 999) / 1000); + return 0; +} +#endif #define VOID // Portable descriptors. A test that only needs a connection ST can wait on uses these, not a pipe or read and write, // so it runs unchanged where only sockets can be polled, such as Windows. A test about a POSIX feature itself, such as // a pipe, a signal or a chosen descriptor number, keeps the POSIX calls. +#ifdef _WIN32 +// Maps the last Winsock error to errno, as ST does. +extern "C" int _st_win64_errno(int wsaerr, int connecting); + +// Native Windows (winsock comes from st.h) has no socketpair, so the pair is a loopback TCP connection of two blocking +// sockets, with Nagle off so small writes are not delayed, as on a Unix-domain socketpair. Descriptors are winsock +// sockets that fit in an int. Returns 0, or -1 with errno set. +static inline int st_utest_stream_pair(int fds[2]) +{ + fds[0] = fds[1] = -1; + SOCKET listener = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + SOCKET client = INVALID_SOCKET, server = INVALID_SOCKET; + int err = 0; + + sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + int addrlen = sizeof(addr); + if (listener == INVALID_SOCKET || ::bind(listener, (sockaddr*)&addr, addrlen) != 0 || + ::listen(listener, SOMAXCONN) != 0 || ::getsockname(listener, (sockaddr*)&addr, &addrlen) != 0 || + (client = ::socket(AF_INET, SOCK_STREAM, IPPROTO_TCP)) == INVALID_SOCKET || + ::connect(client, (sockaddr*)&addr, addrlen) != 0) { + err = ::WSAGetLastError(); + } + + // Accept the client's own connection: another process may connect to the port first, so check the peer address. + sockaddr_in self; + memset(&self, 0, sizeof(self)); + int selflen = sizeof(self); + if (!err && ::getsockname(client, (sockaddr*)&self, &selflen) != 0) err = ::WSAGetLastError(); + for (int i = 0; !err && server == INVALID_SOCKET && i < 16; i++) { + sockaddr_in peer; + int peerlen = sizeof(peer); + SOCKET s = ::accept(listener, (sockaddr*)&peer, &peerlen); + if (s == INVALID_SOCKET) { + err = ::WSAGetLastError(); + } else if (peer.sin_port == self.sin_port && peer.sin_addr.s_addr == self.sin_addr.s_addr) { + server = s; + } else { + ::closesocket(s); + } + } + if (!err && server == INVALID_SOCKET) err = WSAECONNREFUSED; + + BOOL nodelay = TRUE; + if (!err && (::setsockopt(client, IPPROTO_TCP, TCP_NODELAY, (const char*)&nodelay, sizeof(nodelay)) != 0 || + ::setsockopt(server, IPPROTO_TCP, TCP_NODELAY, (const char*)&nodelay, sizeof(nodelay)) != 0)) { + err = ::WSAGetLastError(); + } + + if (listener != INVALID_SOCKET) ::closesocket(listener); + if (err) { + if (client != INVALID_SOCKET) ::closesocket(client); + if (server != INVALID_SOCKET) ::closesocket(server); + errno = _st_win64_errno(err, 0); + return -1; + } + fds[0] = (int)client; + fds[1] = (int)server; + return 0; +} + +// Winsock takes the option value as char*, so these overloads let tests pass any pointer as on POSIX. +static inline int setsockopt(int fd, int level, int name, const void* value, socklen_t size) +{ + return ::setsockopt((SOCKET)fd, level, name, (const char*)value, size); +} + +static inline int getsockopt(int fd, int level, int name, void* value, socklen_t* size) +{ + return ::getsockopt((SOCKET)fd, level, name, (char*)value, size); +} + +static inline ssize_t st_utest_send(int fd, const void* buf, size_t size) +{ + return ::send((SOCKET)fd, (const char*)buf, (int)size, 0); +} + +static inline ssize_t st_utest_recv(int fd, void* buf, size_t size) +{ + return ::recv((SOCKET)fd, (char*)buf, (int)size, 0); +} + +static inline int st_utest_close(int fd) +{ + return ::closesocket((SOCKET)fd); +} + +// Whether the last send or recv failed because the socket wasn't ready. +static inline bool st_utest_would_block() +{ + return ::WSAGetLastError() == WSAEWOULDBLOCK; +} +#else // Two connected stream sockets. Returns 0, or -1 with errno set. static inline int st_utest_stream_pair(int fds[2]) { @@ -50,6 +168,7 @@ static inline bool st_utest_would_block() { return errno == EAGAIN || errno == EWOULDBLOCK; } +#endif // A connection whose one end ST waits on, wrapped with st_netfd_open_socket, and whose other end, the peer, the test // reads and writes directly. diff --git a/utest/st_utest_coroutines.cpp b/utest/st_utest_coroutines.cpp index ff852bc..0fb68f6 100644 --- a/utest/st_utest_coroutines.cpp +++ b/utest/st_utest_coroutines.cpp @@ -4,6 +4,7 @@ #include #include +#include //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for empty coroutine. @@ -118,3 +119,170 @@ VOID TEST(CoroutineTest, StartCoroutineParams) EXPECT_EQ(110, r0); } +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The utest for live values across a switch. A compiler keeps loop state in callee-saved registers across a call, so a +// context switch that misses one of them, such as rdi, rsi or xmm6-xmm15 on Windows x64, corrupts the coroutine that +// resumes. The utest is built without optimization, which keeps locals in memory, so the function that holds the values +// is optimized by itself; clang has no such attribute and checks the values in memory only. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#if defined(_MSC_VER) +#define COROUTINE_TEST_OPTIMIZE +#elif defined(__GNUC__) && !defined(__clang__) +#define COROUTINE_TEST_OPTIMIZE __attribute__((optimize("O2"))) +#else +#define COROUTINE_TEST_OPTIMIZE +#endif + +#define COROUTINE_TEST_LIVE_ROUNDS 100 +#define COROUTINE_TEST_LIVE_COROUTINES 4 + +// Seven integers and six doubles, more than the volatile registers can hold across a call. +struct CoroutineTestLive { + uint64_t seed_; + uint64_t ints_; + double doubles_; + CoroutineTestLive() : seed_(0), ints_(0), doubles_(0) { + } +}; + +static void coroutine_test_no_yield() +{ +} + +static void coroutine_test_yield() +{ + st_usleep(0); +} + +#if defined(_MSC_VER) +#pragma optimize("gt", on) +#endif +static COROUTINE_TEST_OPTIMIZE void coroutine_test_live_values(CoroutineTestLive* live, void (*yield)()) +{ + uint64_t a = live->seed_, b = a + 1, c = a + 2, d = a + 3, e = a + 5, f = a + 7, g = a + 11; + double x = (double)a, y = x + 0.5, z = x + 1.5, u = x + 2.5, v = x + 3.5, w = x + 4.5; + for (int i = 0; i < COROUTINE_TEST_LIVE_ROUNDS; i++) { + yield(); + a = a * 3 + b; b = b * 5 + c; c = c * 7 + d; d = d * 11 + e; e = e * 13 + f; f = f * 17 + g; g = g * 19 + a; + x = x * 0.5 + y; y = y * 0.25 + z; z = z * 0.5 + u; u = u * 0.25 + v; v = v * 0.5 + w; w = w * 0.25 + x + i; + } + live->ints_ = a ^ b ^ c ^ d ^ e ^ f ^ g; + live->doubles_ = x + y + z + u + v + w; +} +#if defined(_MSC_VER) +#pragma optimize("", on) +#endif + +static void* coroutine_test_live_coroutine(void* arg) +{ + coroutine_test_live_values((CoroutineTestLive*)arg, coroutine_test_yield); + return NULL; +} + +VOID TEST(CoroutineTest, LiveValuesSurviveSwitches) +{ + // Every coroutine holds different values, so a register another coroutine left behind is caught. + CoroutineTestLive lives[COROUTINE_TEST_LIVE_COROUTINES]; + st_thread_t trds[COROUTINE_TEST_LIVE_COROUTINES]; + for (int i = 0; i < COROUTINE_TEST_LIVE_COROUTINES; i++) { + lives[i].seed_ = 1000 * (i + 1); + trds[i] = st_thread_create(coroutine_test_live_coroutine, &lives[i], 1, 0); + ASSERT_TRUE(trds[i] != NULL); + } + for (int i = 0; i < COROUTINE_TEST_LIVE_COROUTINES; i++) { + st_thread_join(trds[i], NULL); + } + + // The same computation without switches gives the expected values. + for (int i = 0; i < COROUTINE_TEST_LIVE_COROUTINES; i++) { + CoroutineTestLive expected; + expected.seed_ = lives[i].seed_; + coroutine_test_live_values(&expected, coroutine_test_no_yield); + EXPECT_EQ(expected.ints_, lives[i].ints_) << "coroutine " << i; + EXPECT_EQ(expected.doubles_, lives[i].doubles_) << "coroutine " << i; + } +} + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The utest for deep recursion on a coroutine stack. Every frame fills a buffer, the deepest frame switches to the other +// coroutines, and every frame checks its buffer on the way back. A frame larger than a page makes Windows probe the +// stack page by page against the stack bounds of the coroutine. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#define COROUTINE_TEST_RECURSE_COROUTINES 3 + +struct CoroutineTestRecurse { + int id_; + int depth_; + int frames_; + int corrupted_; + CoroutineTestRecurse() : id_(0), depth_(0), frames_(0), corrupted_(0) { + } +}; + +template +static void coroutine_test_recurse(CoroutineTestRecurse* r, int depth) +{ + volatile unsigned char buf[SIZE]; + unsigned char v = (unsigned char)(r->id_ * 31 + depth); + for (int i = 0; i < SIZE; i++) { + buf[i] = (unsigned char)(v + i); + } + + if (depth < r->depth_) { + coroutine_test_recurse(r, depth + 1); + } else { + st_usleep(0); + st_usleep(0); + } + + for (int i = 0; i < SIZE; i++) { + if (buf[i] != (unsigned char)(v + i)) { + r->corrupted_++; + break; + } + } + r->frames_++; +} + +template +static void* coroutine_test_recurse_coroutine(void* arg) +{ + CoroutineTestRecurse* r = (CoroutineTestRecurse*)arg; + coroutine_test_recurse(r, 1); + return NULL; +} + +template +static void coroutine_test_recurse_coroutines(int depth, int stk_size) +{ + CoroutineTestRecurse rs[COROUTINE_TEST_RECURSE_COROUTINES]; + st_thread_t trds[COROUTINE_TEST_RECURSE_COROUTINES]; + for (int i = 0; i < COROUTINE_TEST_RECURSE_COROUTINES; i++) { + rs[i].id_ = i + 1; + rs[i].depth_ = depth; + trds[i] = st_thread_create(coroutine_test_recurse_coroutine, &rs[i], 1, stk_size); + ASSERT_TRUE(trds[i] != NULL); + } + for (int i = 0; i < COROUTINE_TEST_RECURSE_COROUTINES; i++) { + st_thread_join(trds[i], NULL); + } + + for (int i = 0; i < COROUTINE_TEST_RECURSE_COROUTINES; i++) { + EXPECT_EQ(depth, rs[i].frames_) << "coroutine " << i; + EXPECT_EQ(0, rs[i].corrupted_) << "coroutine " << i; + } +} + +VOID TEST(CoroutineTest, DeepRecursionOnDefaultStack) +{ + // About 70 KB of the default 128 KB stack. + coroutine_test_recurse_coroutines<256>(200, 0); +} + +VOID TEST(CoroutineTest, DeepRecursionWithLargeFramesOnLargeStack) +{ + // About 640 KB of a 1 MB stack, in frames larger than a page. + coroutine_test_recurse_coroutines<5000>(128, 1024 * 1024); +} diff --git a/utest/st_utest_event.cpp b/utest/st_utest_event.cpp index 8e38e7a..070cc8b 100644 --- a/utest/st_utest_event.cpp +++ b/utest/st_utest_event.cpp @@ -6,20 +6,26 @@ #include #include #include +#ifndef _WIN32 #include +#endif #include #include #include #include +#ifndef _WIN32 #include +#endif #include +#ifndef _WIN32 #include #include #include #include #include #include +#endif #define ST_UTIME_MILLISECONDS 1000 #define ST_UTEST_TIMEOUT (100 * ST_UTIME_MILLISECONDS) @@ -31,6 +37,7 @@ // on descriptors already in it. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifndef _WIN32 // POSIX only: pipes at chosen descriptor numbers // A pipe whose read end is moved to a chosen descriptor number, and whose write end stays plain. struct EventTestHighPipe { int reader_; @@ -68,6 +75,7 @@ static bool event_test_high_pipe(EventTestHighPipe& p, int fd) p.reader_ = fd; return true; } +#endif struct EventTestReader { st_netfd_t stfd_; @@ -89,6 +97,7 @@ static void* event_test_reader_coroutine(void* arg) return NULL; } +#ifndef _WIN32 // POSIX only: pipes at chosen descriptor numbers // The server already holds more descriptors than the initial table, so the next connection gets descriptor 5000. // st_netfd_open grows the table for it, a coroutine reading it waits and wakes on its data, and the descriptor then // closes, since no waiter is left counted on it. Locks in current behavior. @@ -160,6 +169,7 @@ VOID TEST(HighFdTest, WaiterKeepsWaitingWhenTableGrows) EXPECT_EQ(0, st_netfd_close(r.stfd_)); ::close(fds[1]); } +#endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for a pollset the event system refuses part of, such as a tool that waits with st_poll on its server @@ -168,6 +178,7 @@ VOID TEST(HighFdTest, WaiterKeepsWaitingWhenTableGrows) // at once and leaves every descriptor as it was. kqueue accepts a regular file and reports it ready. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifndef _WIN32 // POSIX only: regular files and pipes // A regular file holding one command, read from the start, like stdin redirected from a file. static int event_test_command_file() { @@ -324,6 +335,7 @@ VOID TEST(PollRefusedTest, ReaderOnSameConnectionKeepsWaiting) ::close(fds[1]); ::close(file); } +#endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for waiting for TCP urgent data with POLLPRI, such as an FTP server whose client aborts a transfer, or a @@ -341,8 +353,8 @@ struct EventTestTcpPair { EventTestTcpPair() : server_(-1), client_(-1) { } ~EventTestTcpPair() { - if (server_ >= 0) ::close(server_); - if (client_ >= 0) ::close(client_); + if (server_ >= 0) st_utest_close(server_); + if (client_ >= 0) st_utest_close(client_); } }; @@ -360,7 +372,7 @@ static int event_test_accept_from(int lfd, int cfd) if (sfd < 0 || (peer.sin_port == local.sin_port && peer.sin_addr.s_addr == local.sin_addr.s_addr)) { return sfd; } - ::close(sfd); + st_utest_close(sfd); } } @@ -378,18 +390,18 @@ static bool event_test_tcp_pair(EventTestTcpPair& pair) socklen_t addrlen = sizeof(addr); if (::bind(lfd, (sockaddr*)&addr, sizeof(addr)) < 0 || ::listen(lfd, 1) < 0 || getsockname(lfd, (sockaddr*)&addr, &addrlen) < 0) { - ::close(lfd); + st_utest_close(lfd); return false; } pair.client_ = socket(AF_INET, SOCK_STREAM, 0); if (pair.client_ < 0 || ::connect(pair.client_, (sockaddr*)&addr, sizeof(addr)) < 0) { - ::close(lfd); + st_utest_close(lfd); return false; } pair.server_ = event_test_accept_from(lfd, pair.client_); - ::close(lfd); + st_utest_close(lfd); return pair.server_ >= 0; } @@ -527,6 +539,7 @@ VOID TEST(PollPriTest, ReaderAndAbortWaiterShareConnection) // list for a large pollset in the same way. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifndef _WIN32 // POSIX only: dup of a pipe // A server waits on 4097 connections in one st_poll, one more than the initial result list, and data arrives on all of // them at once. Each connection is a dup of one pipe's read end, so one byte makes them all readable. st_poll reports // every connection ready, and the descriptors then close. Locks in current behavior. @@ -560,6 +573,7 @@ VOID TEST(ManyWaitsTest, EveryReadyConnectionReported) ::close(fds[0]); ::close(fds[1]); } +#endif #if defined(__linux__) //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// diff --git a/utest/st_utest_exception.cpp b/utest/st_utest_exception.cpp index 1b40986..e6068b3 100644 --- a/utest/st_utest_exception.cpp +++ b/utest/st_utest_exception.cpp @@ -268,3 +268,80 @@ VOID TEST(ExceptionTest, YieldInDestructorWhileUnwinding) EXPECT_EQ(300, u.call_.caught_); EXPECT_EQ(301, other.caught_); } + +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// A throw from the bottom of a deep recursion unwinds frames larger than a page across most of a large stack, while +// other coroutines do the same on their own stacks, so each one runs every destructor of its own frames in order. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + +#define EXCEPTION_TEST_DEEP_DEPTH 128 +#define EXCEPTION_TEST_DEEP_FRAME 5000 + +struct ExceptionTestDeep { + int id_; + int destroyed_; + bool ordered_; + int caught_; + ExceptionTestDeep() : id_(0), destroyed_(0), ordered_(true), caught_(-1) { + } +}; + +class ExceptionTestDeepGuard { + ExceptionTestDeep* deep_; + int depth_; + volatile char buf_[EXCEPTION_TEST_DEEP_FRAME]; +public: + ExceptionTestDeepGuard(ExceptionTestDeep* deep, int depth) : deep_(deep), depth_(depth) { + buf_[0] = buf_[EXCEPTION_TEST_DEEP_FRAME - 1] = (char)(deep->id_ + depth); + } + ~ExceptionTestDeepGuard() { + // The deepest frame unwinds first, and its buffer is still the one it wrote. + if (depth_ != EXCEPTION_TEST_DEEP_DEPTH - deep_->destroyed_) deep_->ordered_ = false; + if (buf_[0] != (char)(deep_->id_ + depth_) || buf_[EXCEPTION_TEST_DEEP_FRAME - 1] != buf_[0]) { + deep_->ordered_ = false; + } + deep_->destroyed_++; + } +}; + +static void exception_test_deep_recurse(ExceptionTestDeep* deep, int depth) +{ + ExceptionTestDeepGuard guard(deep, depth); + if (depth == EXCEPTION_TEST_DEEP_DEPTH) { + st_usleep(0); + exception_test_throw(deep->id_); + } + exception_test_deep_recurse(deep, depth + 1); +} + +static void* exception_test_deep_coroutine(void* arg) +{ + ExceptionTestDeep* deep = (ExceptionTestDeep*)arg; + try { + exception_test_deep_recurse(deep, 1); + } catch (const std::runtime_error& e) { + deep->caught_ = std::stoi(e.what()); + } + return NULL; +} + +VOID TEST(ExceptionTest, ThrowFromDeepRecursionWithLargeFrames) +{ + ExceptionTestDeep deeps[3]; + st_thread_t trds[3]; + for (int i = 0; i < 3; i++) { + deeps[i].id_ = 400 + i; + // About 640 KB of a 1 MB stack. + trds[i] = st_thread_create(exception_test_deep_coroutine, &deeps[i], 1, 1024 * 1024); + ASSERT_TRUE(trds[i] != NULL); + } + for (int i = 0; i < 3; i++) { + st_thread_join(trds[i], NULL); + } + + for (int i = 0; i < 3; i++) { + EXPECT_EQ(400 + i, deeps[i].caught_) << "coroutine " << i; + EXPECT_EQ(EXCEPTION_TEST_DEEP_DEPTH, deeps[i].destroyed_) << "coroutine " << i; + EXPECT_TRUE(deeps[i].ordered_) << "coroutine " << i; + } +} diff --git a/utest/st_utest_io.cpp b/utest/st_utest_io.cpp index a758a22..2322da5 100644 --- a/utest/st_utest_io.cpp +++ b/utest/st_utest_io.cpp @@ -6,27 +6,41 @@ #include #include #include +#ifndef _WIN32 #include #include +#endif #include +#ifndef _WIN32 #include +#endif #include #include #include #include +#ifndef _WIN32 #include +#endif #include +#ifndef _WIN32 #include #include #include +#endif #include +#ifndef _WIN32 #include #include +#endif #define ST_UTIME_MILLISECONDS 1000 #define ST_UTEST_TIMEOUT (100 * ST_UTIME_MILLISECONDS) +#ifdef _MSC_VER // Windows only: some tests skip with GTEST_SKIP, so the rest of their body is unreachable +#pragma warning(disable: 4702) +#endif + //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for reading a TCP stream, the way SRS reads every RTMP chunk header and payload with st_read_fully. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -70,7 +84,7 @@ static int io_accept_from(int lfd, int cfd) if (sfd < 0 || (peer.sin_port == local.sin_port && peer.sin_addr.s_addr == local.sin_addr.s_addr)) { return sfd; } - ::close(sfd); + st_utest_close(sfd); } } @@ -90,21 +104,21 @@ static bool io_tcp_pair(IoTestTcpPair& pair) socklen_t addrlen = sizeof(addr); if (::bind(lfd, (sockaddr*)&addr, sizeof(addr)) < 0 || ::listen(lfd, 1) < 0 || getsockname(lfd, (sockaddr*)&addr, &addrlen) < 0) { - ::close(lfd); + st_utest_close(lfd); return false; } int cfd = socket(AF_INET, SOCK_STREAM, 0); if (cfd < 0 || ::connect(cfd, (sockaddr*)&addr, sizeof(addr)) < 0) { - if (cfd >= 0) ::close(cfd); - ::close(lfd); + if (cfd >= 0) st_utest_close(cfd); + st_utest_close(lfd); return false; } int sfd = io_accept_from(lfd, cfd); - ::close(lfd); + st_utest_close(lfd); if (sfd < 0) { - ::close(cfd); + st_utest_close(cfd); return false; } @@ -481,6 +495,7 @@ VOID TEST(IoReadTest, ReadOnResetConnectionFails) } } +#ifndef _WIN32 // POSIX only: signals, fcntl and pthread static volatile sig_atomic_t io_read_signals = 0; static void io_read_on_signal(int signo) @@ -657,6 +672,7 @@ VOID TEST(IoReadTest, ReadvResidRetriesWhenSignalInterruptsSystemCall) EXPECT_STREQ("hdr", header); EXPECT_STREQ("payload", payload); } +#endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for accepting and connecting TCP, the way an SRS listener accepts clients and an SRS edge or forwarder @@ -677,12 +693,12 @@ static st_netfd_t io_tcp_listen(struct sockaddr_in& addr, int backlog) socklen_t addrlen = sizeof(addr); if (::bind(lfd, (sockaddr*)&addr, sizeof(addr)) < 0 || ::listen(lfd, backlog) < 0 || getsockname(lfd, (sockaddr*)&addr, &addrlen) < 0) { - ::close(lfd); + st_utest_close(lfd); return NULL; } st_netfd_t stfd = st_netfd_open_socket(lfd); - if (!stfd) ::close(lfd); + if (!stfd) st_utest_close(lfd); return stfd; } @@ -702,7 +718,7 @@ static void* io_connect_coroutine(void* arg) int fd = socket(AF_INET, SOCK_STREAM, 0); c->stfd_ = (fd < 0) ? NULL : st_netfd_open_socket(fd); if (!c->stfd_) { - if (fd >= 0) ::close(fd); + if (fd >= 0) st_utest_close(fd); return NULL; } @@ -712,6 +728,7 @@ static void* io_connect_coroutine(void* arg) return NULL; } +#ifndef _WIN32 // POSIX only: fcntl O_NONBLOCK // An SRS listener waits for clients with no timeout. A client connects later: st_accept wakes, reports the client's // address, and returns a non-blocking socket that is ready for ST I/O, whether or not the OS lets an accepted socket // inherit O_NONBLOCK from the listener. On the client side, st_connect waits for the handshake and succeeds. Locks in @@ -757,6 +774,7 @@ VOID TEST(IoAcceptTest, AcceptWaitsForClient) EXPECT_EQ(5, st_read_fully(client, buf, 5, ST_UTEST_TIMEOUT)); EXPECT_STREQ("hello", buf); } +#endif // No client arrives before the timeout, so st_accept fails with ETIME. Locks in current behavior. VOID TEST(IoAcceptTest, AcceptTimesOut) @@ -824,6 +842,7 @@ VOID TEST(IoAcceptTest, AcceptOnSocketNotListeningFails) EXPECT_EQ(EINVAL, errno); } +#ifndef _WIN32 // POSIX only: signals, fcntl and pthread // Another thread of the program signals the accepting thread a few times, then a client connects. struct IoTestAcceptSignaler { pthread_t target_; @@ -900,6 +919,7 @@ VOID TEST(IoAcceptTest, AcceptRetriesWhenSignalInterruptsSystemCall) EXPECT_EQ(5, st_read_fully(client, buf, 5, ST_UTEST_TIMEOUT)); EXPECT_STREQ("hello", buf); } +#endif // Open a TCP socket for st_connect. static st_netfd_t io_tcp_socket(int family) @@ -907,7 +927,7 @@ static st_netfd_t io_tcp_socket(int family) int fd = socket(family, SOCK_STREAM, 0); if (fd < 0) return NULL; st_netfd_t stfd = st_netfd_open_socket(fd); - if (!stfd) ::close(fd); + if (!stfd) st_utest_close(fd); return stfd; } @@ -916,6 +936,9 @@ static st_netfd_t io_tcp_socket(int family) // report the same error. The test takes a free port by opening a listener and closing it. Locks in current behavior. VOID TEST(IoConnectTest, ConnectRefusedWhenNobodyListens) { +#ifdef _WIN32 // Windows only: skipped, Windows retries a refused connect for about 2 s, longer than the timeout + GTEST_SKIP() << "Windows retries a refused connect for about 2 seconds"; +#endif struct sockaddr_in addr; st_netfd_t listener = io_tcp_listen(addr, 8); ASSERT_TRUE(listener != NULL); @@ -1088,6 +1111,25 @@ static bool io_shrink_send_buffer(IoTestTcpPair& pair) return setsockopt(st_netfd_fileno(pair.server_), SOL_SOCKET, SO_SNDBUF, &size, sizeof(size)) == 0; } +#ifdef _WIN32 // Windows only: a large send does not wait +// Windows takes a whole non-blocking send, however large, while the send buffer holds less than SO_SNDBUF, so a +// single large write to a slow player never waits; the wait starts from the next write. So on Windows the tests first +// fill the send and receive buffers, and the write under test waits, times out, or sees the reset as on POSIX. +// Winsock never sends part of a non-blocking send: it takes the whole buffer, or fails with WSAEWOULDBLOCK and sends +// nothing. So the tests that need a partial count are skipped on Windows; the wait and retry after a would-block is +// covered by the tests that fill the buffers first. +static bool io_fill_buffers(int fd, std::string& filled); + +// Fill the buffers of the server end. The fill sleeps outside ST, so let ST read its clock again after it, or a timeout +// set right after would start from the old clock and expire early. +static bool io_fill_send_buffer(IoTestTcpPair& pair, std::string& filled) +{ + bool ok = io_fill_buffers(st_netfd_fileno(pair.server_), filled); + st_usleep(0); + return ok; +} +#endif + // Messages as SRS sends them: each one is a 12-byte header iovec and a payload iovec. Both point into one byte // pattern that doesn't repeat at buffer boundaries, so the player can compare what it got with data_, and a byte that // is lost, sent twice or sent out of order shows up. @@ -1173,6 +1215,9 @@ VOID TEST(IoWriteTest, WritevSendsMessageInOneCall) // byte is sent, in order. The player has to start reading before st_writev can return. Locks in current behavior. VOID TEST(IoWriteTest, WritevWaitsForSlowPlayer) { +#ifdef _WIN32 // Windows only: skipped, the test needs a partial send, which Winsock never does + GTEST_SKIP() << "Winsock never sends part of a non-blocking send"; +#endif IoTestTcpPair pair; ASSERT_TRUE(io_tcp_pair(pair)); ASSERT_TRUE(io_shrink_send_buffer(pair)); @@ -1194,6 +1239,9 @@ VOID TEST(IoWriteTest, WritevWaitsForSlowPlayer) // and frees it when done. Every byte still arrives in order. Locks in current behavior. VOID TEST(IoWriteTest, WritevWaitsForSlowPlayerWithMergedWrite) { +#ifdef _WIN32 // Windows only: skipped, the test needs a partial send, which Winsock never does + GTEST_SKIP() << "Winsock never sends part of a non-blocking send"; +#endif IoTestTcpPair pair; ASSERT_TRUE(io_tcp_pair(pair)); ASSERT_TRUE(io_shrink_send_buffer(pair)); @@ -1234,6 +1282,10 @@ VOID TEST(IoWriteTest, WritevFailsWhenPlayerResetsMidStream) IoTestTcpPair pair; ASSERT_TRUE(io_tcp_pair(pair)); ASSERT_TRUE(io_shrink_send_buffer(pair)); +#ifdef _WIN32 // Windows only: fill the buffers, or the write does not wait + std::string filled; + ASSERT_TRUE(io_fill_send_buffer(pair, filled)); +#endif IoTestMessages msgs(4, 256 * 1024); IoTestBlockedWriter w; @@ -1258,6 +1310,9 @@ VOID TEST(IoWriteTest, WritevFailsWhenPlayerResetsMidStream) // behavior. VOID TEST(IoWriteTest, WritevTimesOutAndLosesTheSentCount) { +#ifdef _WIN32 // Windows only: skipped, the test needs a partial send, which Winsock never does + GTEST_SKIP() << "Winsock never sends part of a non-blocking send"; +#endif IoTestTcpPair pair; ASSERT_TRUE(io_tcp_pair(pair)); ASSERT_TRUE(io_shrink_send_buffer(pair)); @@ -1279,6 +1334,10 @@ VOID TEST(IoWriteTest, WritevInterruptedWhenConnectionStops) IoTestTcpPair pair; ASSERT_TRUE(io_tcp_pair(pair)); ASSERT_TRUE(io_shrink_send_buffer(pair)); +#ifdef _WIN32 // Windows only: fill the buffers, or the write does not wait + std::string filled; + ASSERT_TRUE(io_fill_send_buffer(pair, filled)); +#endif IoTestMessages msgs(4, 256 * 1024); IoTestBlockedWriter w; @@ -1297,6 +1356,7 @@ VOID TEST(IoWriteTest, WritevInterruptedWhenConnectionStops) EXPECT_EQ(EINTR, w.errno_); } +#ifndef _WIN32 // POSIX only: signals and pthread // Another thread of the program signals the writing thread a few times, then reads everything as the player. struct IoTestWriteSignaler { pthread_t target_; @@ -1327,6 +1387,7 @@ static void* io_write_signaler_thread(void* arg) } return NULL; } +#endif // Write to the non-blocking socket until the send buffer and the player's receive buffer are full and the kernel takes // no more, and keep what was written. The kernel moves the send buffer to the player in the background, so the first @@ -1355,6 +1416,7 @@ static bool io_fill_buffers(int fd, std::string& filled) } } +#ifndef _WIN32 // POSIX only: signals, fcntl and pthread // A signal interrupts writev while it waits in the kernel for a stalled player, and the handler was installed without // SA_RESTART, so writev fails with EINTR. st_writev retries it instead of failing, because only st_thread_interrupt // means the writer should stop, and every byte arrives once the player reads. A blocking writev fails with EINTR only @@ -1557,6 +1619,7 @@ VOID TEST(IoWriteTest, SendmsgRetriesWhenSignalInterruptsSystemCall) EXPECT_TRUE(s.received_.substr(0, filled.size()) == filled); EXPECT_TRUE(s.received_.substr(filled.size()) == msgs.data_); } +#endif // st_writev_resid keeps the count that st_writev loses. When the stalled player makes it time out, the caller's iovec // array points at what is still unsent: the rest of a partly sent buffer, then the untouched ones. When the player @@ -1564,6 +1627,9 @@ VOID TEST(IoWriteTest, SendmsgRetriesWhenSignalInterruptsSystemCall) // order. Locks in current behavior. VOID TEST(IoWriteTest, WritevResidResumesAfterTimeout) { +#ifdef _WIN32 // Windows only: skipped, the test needs a partial send, which Winsock never does + GTEST_SKIP() << "Winsock never sends part of a non-blocking send"; +#endif IoTestTcpPair pair; ASSERT_TRUE(io_tcp_pair(pair)); ASSERT_TRUE(io_shrink_send_buffer(pair)); @@ -1632,6 +1698,9 @@ VOID TEST(IoWriteTest, WritevWithNothingToSendReturnsAtOnce) // current behavior. VOID TEST(IoWriteTest, WriteWaitsForSlowPlayer) { +#ifdef _WIN32 // Windows only: skipped, the test needs a partial send, which Winsock never does + GTEST_SKIP() << "Winsock never sends part of a non-blocking send"; +#endif IoTestTcpPair pair; ASSERT_TRUE(io_tcp_pair(pair)); ASSERT_TRUE(io_shrink_send_buffer(pair)); @@ -1665,6 +1734,10 @@ VOID TEST(IoWriteTest, WriteFailsWhenPlayerResetsMidStream) IoTestTcpPair pair; ASSERT_TRUE(io_tcp_pair(pair)); ASSERT_TRUE(io_shrink_send_buffer(pair)); +#ifdef _WIN32 // Windows only: fill the buffers, or the write does not wait + std::string filled; + ASSERT_TRUE(io_fill_send_buffer(pair, filled)); +#endif IoTestMessages msgs(1, 1024 * 1024); IoTestBlockedWriter w; @@ -1688,6 +1761,7 @@ VOID TEST(IoWriteTest, WriteFailsWhenPlayerResetsMidStream) // SRS uses to turn a signal into an event for a coroutine. //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#ifndef _WIN32 // POSIX only: pipes // A pipe whose read end is wrapped with st_netfd_open, the way SRS wraps its signal pipe. The write end stays a plain // descriptor, like the one a signal handler writes to. struct IoTestPipe { @@ -1713,6 +1787,7 @@ static bool io_pipe(IoTestPipe& p) } return true; } +#endif struct IoTestSignalReader { st_netfd_t stfd_; @@ -1730,6 +1805,7 @@ static void* io_signal_reader_coroutine(void* arg) return NULL; } +#ifndef _WIN32 // POSIX only: pipes and fcntl // SRS turns signals into events with a pipe: the handler writes the signal number to the write end, and a coroutine // waits on the read end, wrapped with st_netfd_open. A pipe is not a socket, so ST makes it non-blocking with fcntl, // and the coroutine waits instead of blocking the whole process. Locks in current behavior. @@ -1757,6 +1833,7 @@ VOID TEST(IoNetfdTest, SignalPipeWakesWaitingCoroutine) EXPECT_EQ((ssize_t)sizeof(signo), r.nread_); EXPECT_EQ(SIGHUP, r.signo_); } +#endif // Closing a descriptor while another coroutine still waits on it fails with EBUSY, and leaves the descriptor open and // the waiter undisturbed, so the event system never holds a stale descriptor. SRS treats this as a bug and asserts @@ -1826,8 +1903,8 @@ VOID TEST(IoNetfdTest, OpenClosedDescriptorFails) int closed[2]; ASSERT_EQ(0, st_utest_stream_pair(closed)); - ::close(closed[0]); - ::close(closed[1]); + st_utest_close(closed[0]); + st_utest_close(closed[1]); errno = 0; EXPECT_TRUE(st_netfd_open(closed[0]) == NULL); @@ -1842,7 +1919,7 @@ VOID TEST(IoNetfdTest, OpenClosedDescriptorFails) EXPECT_TRUE(reader == first); EXPECT_EQ(0, st_netfd_close(reader)); - ::close(fds[1]); + st_utest_close(fds[1]); } static std::vector _io_freed_specifics; @@ -1880,6 +1957,7 @@ VOID TEST(IoNetfdTest, DescriptorDataIsFreedWhenReplacedOrClosed) EXPECT_EQ("second", _io_freed_specifics[1]); } +#ifndef _WIN32 // POSIX only: getrlimit // st_init raises the soft limit of open descriptors as far as it may, so a server can hold many connections, and // st_getfdlimit reports that limit. On Linux the soft limit becomes the hard limit. On macOS an unlimited hard limit // reads as negative, so ST keeps the soft limit. Locks in current behavior. @@ -1894,6 +1972,7 @@ VOID TEST(IoNetfdTest, FdLimitIsRaisedAtInit) EXPECT_EQ(rlim.rlim_max, rlim.rlim_cur); #endif } +#endif // The original ST serialized accept across processes; this fork dropped that, so st_netfd_serialize_accept is a no-op // kept for source compatibility. It succeeds and the listener accepts as before. Locks in current behavior. @@ -1911,10 +1990,11 @@ VOID TEST(IoNetfdTest, SerializeAcceptIsNoOp) st_netfd_t client = st_accept(listener, NULL, NULL, ST_UTEST_TIMEOUT); EXPECT_TRUE(client != NULL); if (client) st_netfd_close(client); - ::close(cfd); + st_utest_close(cfd); EXPECT_EQ(0, st_netfd_close(listener)); } +#ifndef _WIN32 // POSIX only: FIFOs and regular files with st_open //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for st_open: a FIFO or another special file opened non-blocking, so a coroutine waits on it like on a // socket. For example, a server that takes commands from a named pipe, or reads a stream another program writes into @@ -2171,3 +2251,926 @@ VOID TEST(IoOpenTest, RegularFileNeverWaits) EXPECT_EQ(0, st_read(fd, buf, sizeof(buf), ST_UTEST_TIMEOUT)); EXPECT_LT(st_utime() - starttime, ST_UTEST_TIMEOUT); } +#endif + +#ifdef _WIN32 // Windows only: Winsock errors +// The Winsock error of a failed socket call, mapped to errno; connecting is set for connect. +extern "C" int _st_win64_errno(int wsaerr, int connecting); + +// ST and its callers, such as SRS, check errno, but Winsock reports errors by WSAGetLastError, so ST maps them. +VOID TEST(WinErrnoTest, MapsWinsockErrors) +{ + EXPECT_EQ(0, _st_win64_errno(0, 0)); + EXPECT_EQ(EAGAIN, _st_win64_errno(WSAEWOULDBLOCK, 0)); + EXPECT_EQ(EINPROGRESS, _st_win64_errno(WSAEINPROGRESS, 0)); + EXPECT_EQ(EALREADY, _st_win64_errno(WSAEALREADY, 0)); + EXPECT_EQ(EINTR, _st_win64_errno(WSAEINTR, 0)); + EXPECT_EQ(ETIMEDOUT, _st_win64_errno(WSAETIMEDOUT, 0)); + EXPECT_EQ(EBADF, _st_win64_errno(WSAEBADF, 0)); + EXPECT_EQ(EACCES, _st_win64_errno(WSAEACCES, 0)); + EXPECT_EQ(EFAULT, _st_win64_errno(WSAEFAULT, 0)); + EXPECT_EQ(EINVAL, _st_win64_errno(WSAEINVAL, 0)); + EXPECT_EQ(EMFILE, _st_win64_errno(WSAEMFILE, 0)); + EXPECT_EQ(ENOTSOCK, _st_win64_errno(WSAENOTSOCK, 0)); + EXPECT_EQ(EDESTADDRREQ, _st_win64_errno(WSAEDESTADDRREQ, 0)); + EXPECT_EQ(EMSGSIZE, _st_win64_errno(WSAEMSGSIZE, 0)); + EXPECT_EQ(EPROTOTYPE, _st_win64_errno(WSAEPROTOTYPE, 0)); + EXPECT_EQ(ENOPROTOOPT, _st_win64_errno(WSAENOPROTOOPT, 0)); + EXPECT_EQ(EPROTONOSUPPORT, _st_win64_errno(WSAEPROTONOSUPPORT, 0)); + EXPECT_EQ(EPROTONOSUPPORT, _st_win64_errno(WSAESOCKTNOSUPPORT, 0)); + EXPECT_EQ(EOPNOTSUPP, _st_win64_errno(WSAEOPNOTSUPP, 0)); + EXPECT_EQ(EAFNOSUPPORT, _st_win64_errno(WSAEPFNOSUPPORT, 0)); + EXPECT_EQ(EAFNOSUPPORT, _st_win64_errno(WSAEAFNOSUPPORT, 0)); + EXPECT_EQ(EADDRINUSE, _st_win64_errno(WSAEADDRINUSE, 0)); + EXPECT_EQ(EADDRNOTAVAIL, _st_win64_errno(WSAEADDRNOTAVAIL, 0)); + EXPECT_EQ(ENETDOWN, _st_win64_errno(WSAENETDOWN, 0)); + EXPECT_EQ(ENETUNREACH, _st_win64_errno(WSAENETUNREACH, 0)); + EXPECT_EQ(ENETRESET, _st_win64_errno(WSAENETRESET, 0)); + EXPECT_EQ(ECONNABORTED, _st_win64_errno(WSAECONNABORTED, 0)); + EXPECT_EQ(ECONNRESET, _st_win64_errno(WSAECONNRESET, 0)); + EXPECT_EQ(ENOBUFS, _st_win64_errno(WSAENOBUFS, 0)); + EXPECT_EQ(EISCONN, _st_win64_errno(WSAEISCONN, 0)); + EXPECT_EQ(ENOTCONN, _st_win64_errno(WSAENOTCONN, 0)); + EXPECT_EQ(EPIPE, _st_win64_errno(WSAESHUTDOWN, 0)); + EXPECT_EQ(ECONNREFUSED, _st_win64_errno(WSAECONNREFUSED, 0)); + EXPECT_EQ(ELOOP, _st_win64_errno(WSAELOOP, 0)); + EXPECT_EQ(ENAMETOOLONG, _st_win64_errno(WSAENAMETOOLONG, 0)); + EXPECT_EQ(EHOSTUNREACH, _st_win64_errno(WSAEHOSTDOWN, 0)); + EXPECT_EQ(EHOSTUNREACH, _st_win64_errno(WSAEHOSTUNREACH, 0)); + EXPECT_EQ(ENOTEMPTY, _st_win64_errno(WSAENOTEMPTY, 0)); + EXPECT_EQ(EBADF, _st_win64_errno(WSA_INVALID_HANDLE, 0)); + EXPECT_EQ(ENOMEM, _st_win64_errno(WSA_NOT_ENOUGH_MEMORY, 0)); + EXPECT_EQ(EINVAL, _st_win64_errno(WSA_INVALID_PARAMETER, 0)); + EXPECT_EQ(ECANCELED, _st_win64_errno(WSA_OPERATION_ABORTED, 0)); + EXPECT_EQ(EINVAL, _st_win64_errno(WSANOTINITIALISED, 0)); +} + +// A non-blocking connect that has not finished yet fails with WSAEWOULDBLOCK, which is EINPROGRESS on POSIX, so +// st_connect waits for it. Other calls keep EAGAIN, and connect maps every other error as usual. +VOID TEST(WinErrnoTest, ConnectWouldBlockIsInProgress) +{ + EXPECT_EQ(EINPROGRESS, _st_win64_errno(WSAEWOULDBLOCK, 1)); + EXPECT_EQ(EINPROGRESS, _st_win64_errno(WSAEINPROGRESS, 1)); + EXPECT_EQ(EALREADY, _st_win64_errno(WSAEALREADY, 1)); + EXPECT_EQ(ECONNREFUSED, _st_win64_errno(WSAECONNREFUSED, 1)); + EXPECT_EQ(ETIMEDOUT, _st_win64_errno(WSAETIMEDOUT, 1)); + EXPECT_EQ(EAGAIN, _st_win64_errno(WSAEWOULDBLOCK, 0)); +} + +// An error with no errno equivalent is EIO, never 0, so the failure is not lost; WSAGetLastError still has it. +VOID TEST(WinErrnoTest, UnknownErrorIsIo) +{ + EXPECT_EQ(EIO, _st_win64_errno(WSASYSNOTREADY, 0)); + EXPECT_EQ(EIO, _st_win64_errno(WSAHOST_NOT_FOUND, 0)); + EXPECT_EQ(EIO, _st_win64_errno(-1, 0)); + EXPECT_EQ(EIO, _st_win64_errno(WSASYSNOTREADY, 1)); +} + +// The errors that real non-blocking sockets report map to what ST's I/O loops expect. +VOID TEST(WinErrnoTest, RealSocketErrors) +{ + WSADATA wsa; + ASSERT_EQ(0, WSAStartup(MAKEWORD(2, 2), &wsa)); + + SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + ASSERT_NE(INVALID_SOCKET, listener); + sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + int addrlen = sizeof(addr); + ASSERT_EQ(0, ::bind(listener, (sockaddr*)&addr, addrlen)); + ASSERT_EQ(0, ::listen(listener, 8)); + ASSERT_EQ(0, ::getsockname(listener, (sockaddr*)&addr, &addrlen)); + u_long nonblock = 1; + ASSERT_EQ(0, ioctlsocket(listener, FIONBIO, &nonblock)); + + // No client yet: accept would block. + EXPECT_EQ(INVALID_SOCKET, ::accept(listener, NULL, NULL)); + EXPECT_EQ(EAGAIN, _st_win64_errno(WSAGetLastError(), 0)); + + // Not connected: send and recv fail with ENOTCONN. + SOCKET client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + ASSERT_NE(INVALID_SOCKET, client); + char c = 0; + EXPECT_EQ(SOCKET_ERROR, ::recv(client, &c, 1, 0)); + EXPECT_EQ(ENOTCONN, _st_win64_errno(WSAGetLastError(), 0)); + + // A non-blocking connect is in progress. + ASSERT_EQ(0, ioctlsocket(client, FIONBIO, &nonblock)); + int r = ::connect(client, (sockaddr*)&addr, addrlen); + if (r == SOCKET_ERROR) { + EXPECT_EQ(EINPROGRESS, _st_win64_errno(WSAGetLastError(), 1)); + } + + // Wait for the connection, then a recv with no data would block. + fd_set wfds; + FD_ZERO(&wfds); + FD_SET(client, &wfds); + timeval tv = {5, 0}; + ASSERT_EQ(1, ::select(0, NULL, &wfds, NULL, &tv)); + EXPECT_EQ(SOCKET_ERROR, ::recv(client, &c, 1, 0)); + EXPECT_EQ(EAGAIN, _st_win64_errno(WSAGetLastError(), 0)); + + // Connecting again: already connected. + EXPECT_EQ(SOCKET_ERROR, ::connect(client, (sockaddr*)&addr, addrlen)); + EXPECT_EQ(EISCONN, _st_win64_errno(WSAGetLastError(), 1)); + + // A closed socket is not a socket. + closesocket(client); + EXPECT_EQ(SOCKET_ERROR, ::send(client, &c, 1, 0)); + EXPECT_EQ(ENOTSOCK, _st_win64_errno(WSAGetLastError(), 0)); + + closesocket(listener); + WSACleanup(); +} +#endif + +#ifdef _WIN32 // Windows only: Winsock startup and SOCKET values +// Whether Winsock is started, so a socket can be created. Sets wsaerr to the error if not. +static bool winsock_test_usable(int* wsaerr = NULL) +{ + SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (wsaerr) *wsaerr = (s == INVALID_SOCKET) ? WSAGetLastError() : 0; + if (s == INVALID_SOCKET) return false; + closesocket(s); + return true; +} + +// Winsock needs WSAStartup before any socket call, and ST's callers, such as SRS, do not call it: st_init does, so the +// main of the utest, which only calls st_init, can create sockets. +VOID TEST(WinsockTest, InitStartsWinsock) +{ + int wsaerr = 0; + EXPECT_TRUE(winsock_test_usable(&wsaerr)); + EXPECT_EQ(0, wsaerr); +} + +// ST keeps descriptors as int (st_netfd_open, st_netfd_fileno), but a Windows SOCKET is a 64-bit handle. Kernel handles +// have only 32 significant bits, so a real SOCKET fits in a non-negative int and comes back unchanged, and the int +// still works as the socket, which is how ST and the tests pass it to Winsock. +VOID TEST(WinsockTest, SocketRoundTripsThroughInt) +{ + // Each socket with its type. + std::vector > sockets; + for (int i = 0; i < 128; i++) { + SOCKET tcp = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + ASSERT_NE(INVALID_SOCKET, tcp); + sockets.push_back(std::make_pair(tcp, (int)SOCK_STREAM)); + + SOCKET udp = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + ASSERT_NE(INVALID_SOCKET, udp); + sockets.push_back(std::make_pair(udp, (int)SOCK_DGRAM)); + + // IPv6 may be unavailable on the host. + SOCKET tcp6 = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP); + if (tcp6 != INVALID_SOCKET) sockets.push_back(std::make_pair(tcp6, (int)SOCK_STREAM)); + } + + for (size_t i = 0; i < sockets.size(); i++) { + SOCKET s = sockets[i].first; + int fd = (int)s; + EXPECT_GE(fd, 0); + EXPECT_EQ(s, (SOCKET)fd); + + // The int is still the socket. + int type = 0; + socklen_t size = sizeof(type); + EXPECT_EQ(0, getsockopt(fd, SOL_SOCKET, SO_TYPE, &type, &size)); + EXPECT_EQ(sockets[i].second, type); + } + + for (size_t i = 0; i < sockets.size(); i++) { + EXPECT_EQ(0, st_utest_close((int)sockets[i].first)); + } +} + +#define WINSOCK_TEST_CHILD_OK 42 + +static DWORD WINAPI winsock_test_thread(LPVOID arg) +{ + int* r = (int*)arg; + // A new OS thread has its own ST, so st_init starts Winsock again, and st_destroy cleans up only that start. + r[0] = st_init(); + r[1] = winsock_test_usable(); + st_destroy(); + return 0; +} + +// Runs the utest again as a child process with only the disabled test, and returns its exit code, or -1. +static int winsock_test_run_child(const char* test) +{ + char exe[MAX_PATH]; + DWORD n = GetModuleFileNameA(NULL, exe, sizeof(exe)); + if (n == 0 || n >= sizeof(exe)) return -1; + + std::string cmd = std::string("\"") + exe + "\" --gtest_also_run_disabled_tests --gtest_filter=" + test; + std::vector line(cmd.begin(), cmd.end()); + line.push_back(0); + + fflush(stdout); + fflush(stderr); + + STARTUPINFOA si; + memset(&si, 0, sizeof(si)); + si.cb = sizeof(si); + PROCESS_INFORMATION pi; + if (!CreateProcessA(NULL, &line[0], NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi)) return -1; + + // A hang kills the child and fails the test, instead of hanging the suite. + DWORD code = (DWORD)-1; + if (WaitForSingleObject(pi.hProcess, 10000) == WAIT_OBJECT_0) { + GetExitCodeProcess(pi.hProcess, &code); + } else { + TerminateProcess(pi.hProcess, 1); + WaitForSingleObject(pi.hProcess, INFINITE); + } + + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return (int)code; +} + +// Runs only in the child process of InitStartsAndDestroyCleansUp, because st_destroy on the main thread stops Winsock +// for the rest of the process. Exits with WINSOCK_TEST_CHILD_OK if every check passed. +VOID TEST(WinsockTest, DISABLED_ChildInitAndDestroy) +{ + // The main of the utest called st_init, which started Winsock. + EXPECT_TRUE(winsock_test_usable()); + + // Another OS thread starts and destroys its own ST. + int r[2] = {-1, -1}; + HANDLE trd = CreateThread(NULL, 0, winsock_test_thread, r, 0, NULL); + ASSERT_TRUE(trd != NULL); + EXPECT_EQ(WAIT_OBJECT_0, WaitForSingleObject(trd, 10000)); + CloseHandle(trd); + EXPECT_EQ(0, r[0]); + EXPECT_EQ(1, r[1]); + + // That st_destroy cleaned up only its own start, so Winsock still works here. + EXPECT_TRUE(winsock_test_usable()); + + // The last st_destroy cleans up the last start, so Winsock stops: st_init and st_destroy are balanced. + st_destroy(); + int wsaerr = 0; + EXPECT_FALSE(winsock_test_usable(&wsaerr)); + EXPECT_EQ(WSANOTINITIALISED, wsaerr); + + if (!::testing::Test::HasFailure()) { + fflush(stdout); + fflush(stderr); + _exit(WINSOCK_TEST_CHILD_OK); + } +} + +// st_init starts Winsock and st_destroy cleans it up, once per OS thread's ST, like SRS calling srs_st_destroy at exit. +VOID TEST(WinsockTest, InitStartsAndDestroyCleansUp) +{ + EXPECT_EQ(WINSOCK_TEST_CHILD_OK, winsock_test_run_child("WinsockTest.DISABLED_ChildInitAndDestroy")); +} +#endif + +#ifdef _WIN32 // Windows only: Winsock under the POSIX calls of io.c +// The POSIX calls io.c makes, which Windows implements with Winsock. The st_* I/O functions call them, and wait in +// st_netfd_poll while they fail with EAGAIN. +extern "C" int _st_win64_ioctl(int fd, unsigned long request, int *arg); +extern "C" int _st_win64_fcntl(int fd, int cmd, int arg); +extern "C" int _st_win64_close(int fd); +extern "C" ssize_t _st_win64_read(int fd, void *buf, size_t nbyte); +extern "C" ssize_t _st_win64_write(int fd, const void *buf, size_t nbyte); +extern "C" ssize_t _st_win64_readv(int fd, const struct iovec *iov, int iov_size); +extern "C" ssize_t _st_win64_writev(int fd, const struct iovec *iov, int iov_size); +extern "C" int _st_win64_recvmsg(int fd, struct msghdr *msg, int flags); +extern "C" int _st_win64_sendmsg(int fd, const struct msghdr *msg, int flags); +extern "C" int _st_win64_recvfrom(int fd, void *buf, int len, int flags, struct sockaddr *from, socklen_t *fromlen); + +// The fcntl commands and flag that io.c defines on Windows, which has no fcntl for sockets. +#define WIN_IO_F_GETFL 3 +#define WIN_IO_F_SETFL 4 +#define WIN_IO_O_NONBLOCK 04000 + +// A connected loopback TCP pair of blocking sockets, as int descriptors. +static bool win_io_tcp_pair(int fds[2]) +{ + fds[0] = fds[1] = -1; + SOCKET listener = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (listener == INVALID_SOCKET) return false; + + sockaddr_in addr; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + int addrlen = sizeof(addr); + SOCKET client = INVALID_SOCKET, server = INVALID_SOCKET; + if (::bind(listener, (sockaddr*)&addr, addrlen) == 0 && ::listen(listener, 1) == 0 && + ::getsockname(listener, (sockaddr*)&addr, &addrlen) == 0) { + client = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (client != INVALID_SOCKET && ::connect(client, (sockaddr*)&addr, addrlen) == 0) { + server = ::accept(listener, NULL, NULL); + } + } + closesocket(listener); + + if (server == INVALID_SOCKET) { + if (client != INVALID_SOCKET) closesocket(client); + return false; + } + fds[0] = (int)client; + fds[1] = (int)server; + return true; +} + +// A UDP socket bound to a loopback port, with its address. +static bool win_io_udp_socket(int& fd, sockaddr_in& addr) +{ + SOCKET s = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); + if (s == INVALID_SOCKET) return false; + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + int addrlen = sizeof(addr); + if (::bind(s, (sockaddr*)&addr, addrlen) != 0 || ::getsockname(s, (sockaddr*)&addr, &addrlen) != 0) { + closesocket(s); + return false; + } + fd = (int)s; + return true; +} + +// Waits until the socket has data to read. +static bool win_io_wait_readable(int fd) +{ + fd_set rfds; + FD_ZERO(&rfds); + FD_SET((SOCKET)fd, &rfds); + timeval tv = {5, 0}; + return ::select(0, &rfds, NULL, NULL, &tv) == 1; +} + +// Reads from a stream until it has n bytes, with readv into the vectors, appending what each read returns. +static std::string win_io_readv_all(int fd, struct iovec* iov, int iov_size, size_t n) +{ + std::string got; + while (got.size() < n) { + ssize_t r = _st_win64_readv(fd, iov, iov_size); + if (r <= 0) break; + for (int i = 0; i < iov_size && r > 0; i++) { + size_t take = std::min((size_t)r, iov[i].iov_len); + got.append((char*)iov[i].iov_base, take); + r -= (ssize_t)take; + } + } + return got; +} + +// st_netfd_open_socket makes a socket non-blocking with ioctl(FIONBIO), which is ioctlsocket on Windows: a read with +// nothing queued then fails with EAGAIN at once, so st_read waits in the event system instead of blocking the thread. +VOID TEST(WinIoTest, IoctlSetsNonBlocking) +{ + int fds[2]; + ASSERT_TRUE(win_io_tcp_pair(fds)); + + int on = 1; + EXPECT_EQ(0, _st_win64_ioctl(fds[1], FIONBIO, &on)); + char buf[8]; + errno = 0; + EXPECT_EQ(-1, _st_win64_read(fds[1], buf, sizeof(buf))); + EXPECT_EQ(EAGAIN, errno); + + // Data queued by the peer is read at once. + EXPECT_EQ(2, _st_win64_write(fds[0], "hi", 2)); + ASSERT_TRUE(win_io_wait_readable(fds[1])); + EXPECT_EQ(2, _st_win64_read(fds[1], buf, sizeof(buf))); + EXPECT_EQ("hi", std::string(buf, 2)); + + EXPECT_EQ(0, _st_win64_close(fds[0])); + EXPECT_EQ(0, _st_win64_close(fds[1])); + + // A closed socket is not a socket. + errno = 0; + EXPECT_EQ(-1, _st_win64_ioctl(fds[1], FIONBIO, &on)); + EXPECT_EQ(ENOTSOCK, errno); +} + +// st_netfd_open makes a descriptor non-blocking the POSIX way, fcntl(F_GETFL) then fcntl(F_SETFL, O_NONBLOCK). On +// Windows, where only sockets are supported, F_SETFL sets the socket's non-blocking mode. +VOID TEST(WinIoTest, FcntlSetsNonBlocking) +{ + int fds[2]; + ASSERT_TRUE(win_io_tcp_pair(fds)); + + int flags = _st_win64_fcntl(fds[1], WIN_IO_F_GETFL, 0); + EXPECT_GE(flags, 0); + EXPECT_EQ(0, _st_win64_fcntl(fds[1], WIN_IO_F_SETFL, flags | WIN_IO_O_NONBLOCK)); + char buf[8]; + errno = 0; + EXPECT_EQ(-1, _st_win64_read(fds[1], buf, sizeof(buf))); + EXPECT_EQ(EAGAIN, errno); + + // Another command is not supported. + errno = 0; + EXPECT_EQ(-1, _st_win64_fcntl(fds[1], 1, 0)); + EXPECT_EQ(EINVAL, errno); + + EXPECT_EQ(0, _st_win64_close(fds[0])); + EXPECT_EQ(0, _st_win64_close(fds[1])); + + // Only sockets: a closed one fails. + errno = 0; + EXPECT_EQ(-1, _st_win64_fcntl(fds[1], WIN_IO_F_SETFL, WIN_IO_O_NONBLOCK)); + EXPECT_EQ(ENOTSOCK, errno); +} + +// read and write are recv and send on a socket: the bytes arrive in order, and a read after the peer closes returns 0, +// the end of stream that st_read reports. +VOID TEST(WinIoTest, ReadWriteStream) +{ + int fds[2]; + ASSERT_TRUE(win_io_tcp_pair(fds)); + + EXPECT_EQ(5, _st_win64_write(fds[0], "hello", 5)); + EXPECT_EQ(6, _st_win64_write(fds[0], " world", 6)); + + std::string got; + char buf[64]; + while (got.size() < 11) { + ssize_t n = _st_win64_read(fds[1], buf, sizeof(buf)); + ASSERT_GT(n, 0); + got.append(buf, (size_t)n); + } + EXPECT_EQ("hello world", got); + + // An empty write sends nothing and succeeds. + EXPECT_EQ(0, _st_win64_write(fds[0], "", 0)); + + EXPECT_EQ(0, _st_win64_close(fds[0])); + EXPECT_EQ(0, _st_win64_read(fds[1], buf, sizeof(buf))); + EXPECT_EQ(0, _st_win64_close(fds[1])); +} + +// A socket that is not connected fails with ENOTCONN, which st_read reports instead of waiting. +VOID TEST(WinIoTest, ReadWriteNotConnected) +{ + SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + ASSERT_NE(INVALID_SOCKET, s); + int fd = (int)s; + char buf[8]; + errno = 0; + EXPECT_EQ(-1, _st_win64_read(fd, buf, sizeof(buf))); + EXPECT_EQ(ENOTCONN, errno); + errno = 0; + EXPECT_EQ(-1, _st_win64_write(fd, "x", 1)); + EXPECT_EQ(ENOTCONN, errno); + EXPECT_EQ(0, _st_win64_close(fd)); +} + +// writev gathers and readv scatters, like SRS sending an RTMP header and payload in one call. iovec is base then +// length while WSABUF is length then base, so io.c copies each one; a cast would put the data in the wrong place. +VOID TEST(WinIoTest, ReadvWritevStream) +{ + int fds[2]; + ASSERT_TRUE(win_io_tcp_pair(fds)); + + char h[] = "head", p[] = "payload", t[] = "!"; + struct iovec out[3] = {{h, 4}, {p, 7}, {t, 1}}; + EXPECT_EQ(12, _st_win64_writev(fds[0], out, 3)); + + // The first buffer holds three bytes, the second the rest. + char a[3], b[32]; + struct iovec in[2] = {{a, sizeof(a)}, {b, sizeof(b)}}; + ASSERT_TRUE(win_io_wait_readable(fds[1])); + EXPECT_EQ("headpayload!", win_io_readv_all(fds[1], in, 2, 12)); + + // No vectors: nothing to do, and no error. + EXPECT_EQ(0, _st_win64_readv(fds[1], in, 0)); + EXPECT_EQ(0, _st_win64_writev(fds[0], out, 0)); + + EXPECT_EQ(0, _st_win64_close(fds[0])); + EXPECT_EQ(0, _st_win64_close(fds[1])); +} + +// More vectors than io.c keeps on its stack, as when SRS sends many small frames in one writev: all are sent, in order. +VOID TEST(WinIoTest, ReadvWritevManyVectors) +{ + int fds[2]; + ASSERT_TRUE(win_io_tcp_pair(fds)); + + const int count = 100; + std::vector parts; + std::string want; + for (int i = 0; i < count; i++) { + parts.push_back(std::string((size_t)(i % 7 + 1), (char)('a' + i % 26))); + want += parts.back(); + } + std::vector out(count); + for (int i = 0; i < count; i++) { + out[i].iov_base = (void*)parts[i].data(); + out[i].iov_len = parts[i].size(); + } + EXPECT_EQ((ssize_t)want.size(), _st_win64_writev(fds[0], &out[0], count)); + + // Read back into one-byte vectors, also more than the stack holds. + std::vector bytes(want.size()); + std::vector in(want.size()); + for (size_t i = 0; i < want.size(); i++) { + in[i].iov_base = &bytes[i]; + in[i].iov_len = 1; + } + size_t done = 0; + while (done < want.size()) { + ssize_t n = _st_win64_readv(fds[1], &in[done], (int)(want.size() - done)); + ASSERT_GT(n, 0); + done += (size_t)n; + } + EXPECT_EQ(want, std::string(bytes.begin(), bytes.end())); + + EXPECT_EQ(0, _st_win64_close(fds[0])); + EXPECT_EQ(0, _st_win64_close(fds[1])); +} + +// sendmsg gathers a header and a payload into one datagram to msg_name, as SRS sends RTP; recvmsg scatters it and +// reports the sender in msg_name, like recvfrom. +VOID TEST(WinIoTest, SendmsgRecvmsgDatagram) +{ + int server, player; + sockaddr_in server_addr, player_addr; + ASSERT_TRUE(win_io_udp_socket(server, server_addr)); + ASSERT_TRUE(win_io_udp_socket(player, player_addr)); + + char h[12], p[100]; + memset(h, 'h', sizeof(h)); + memset(p, 'p', sizeof(p)); + struct iovec out[2] = {{h, sizeof(h)}, {p, sizeof(p)}}; + struct msghdr sent; + memset(&sent, 0, sizeof(sent)); + sent.msg_name = &server_addr; + sent.msg_namelen = sizeof(server_addr); + sent.msg_iov = out; + sent.msg_iovlen = 2; + EXPECT_EQ(112, _st_win64_sendmsg(player, &sent, 0)); + + char rh[12], rp[200]; + struct iovec in[2] = {{rh, sizeof(rh)}, {rp, sizeof(rp)}}; + sockaddr_in from; + memset(&from, 0, sizeof(from)); + struct msghdr received; + memset(&received, 0, sizeof(received)); + received.msg_name = &from; + received.msg_namelen = sizeof(from); + received.msg_iov = in; + received.msg_iovlen = 2; + ASSERT_TRUE(win_io_wait_readable(server)); + EXPECT_EQ(112, _st_win64_recvmsg(server, &received, 0)); + EXPECT_EQ(std::string(12, 'h'), std::string(rh, 12)); + EXPECT_EQ(std::string(100, 'p'), std::string(rp, 100)); + EXPECT_EQ((socklen_t)sizeof(from), received.msg_namelen); + EXPECT_EQ(player_addr.sin_port, from.sin_port); + EXPECT_EQ(htonl(INADDR_LOOPBACK), from.sin_addr.s_addr); + EXPECT_EQ(0, received.msg_flags & MSG_TRUNC); + + EXPECT_EQ(0, _st_win64_close(server)); + EXPECT_EQ(0, _st_win64_close(player)); +} + +// recvmsg passes its flags: MSG_PEEK leaves the datagram queued for the next read. +VOID TEST(WinIoTest, RecvmsgPeekKeepsDatagram) +{ + int server, player; + sockaddr_in server_addr, player_addr; + ASSERT_TRUE(win_io_udp_socket(server, server_addr)); + ASSERT_TRUE(win_io_udp_socket(player, player_addr)); + EXPECT_EQ(5, ::sendto((SOCKET)player, "hello", 5, 0, (sockaddr*)&server_addr, sizeof(server_addr))); + + char first = 0; + struct iovec iov = {&first, 1}; + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + ASSERT_TRUE(win_io_wait_readable(server)); + EXPECT_EQ(1, _st_win64_recvmsg(server, &msg, MSG_PEEK)); + EXPECT_EQ('h', first); + + // Non-blocking, so a peek that dropped the datagram fails here instead of hanging. + int on = 1; + EXPECT_EQ(0, _st_win64_ioctl(server, FIONBIO, &on)); + char buf[64] = {0}; + iov.iov_base = buf; + iov.iov_len = sizeof(buf); + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + EXPECT_EQ(5, _st_win64_recvmsg(server, &msg, 0)); + EXPECT_STREQ("hello", buf); + + EXPECT_EQ(0, _st_win64_close(server)); + EXPECT_EQ(0, _st_win64_close(player)); +} + +// A datagram larger than the buffers is truncated as on POSIX, where Winsock fails with WSAEMSGSIZE: recvmsg returns +// the bytes that fit and sets MSG_TRUNC, and recvfrom and read return the bytes that fit. The rest is dropped, and the +// next read gets the next datagram. +VOID TEST(WinIoTest, TruncatedDatagramReturnsWhatFits) +{ + int server, player; + sockaddr_in server_addr, player_addr; + ASSERT_TRUE(win_io_udp_socket(server, server_addr)); + ASSERT_TRUE(win_io_udp_socket(player, player_addr)); + + std::string large(1500, 'x'); + for (int i = 0; i < 4; i++) { + EXPECT_EQ(1500, ::sendto((SOCKET)player, large.data(), 1500, 0, (sockaddr*)&server_addr, sizeof(server_addr))); + } + EXPECT_EQ(4, ::sendto((SOCKET)player, "next", 4, 0, (sockaddr*)&server_addr, sizeof(server_addr))); + ASSERT_TRUE(win_io_wait_readable(server)); + + char buf[100]; + struct iovec iov = {buf, sizeof(buf)}; + sockaddr_in from; + memset(&from, 0, sizeof(from)); + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &from; + msg.msg_namelen = sizeof(from); + msg.msg_iov = &iov; + msg.msg_iovlen = 1; + EXPECT_EQ(100, _st_win64_recvmsg(server, &msg, 0)); + EXPECT_TRUE((msg.msg_flags & MSG_TRUNC) != 0); + EXPECT_EQ(std::string(100, 'x'), std::string(buf, 100)); + EXPECT_EQ(player_addr.sin_port, from.sin_port); + + memset(&from, 0, sizeof(from)); + socklen_t fromlen = sizeof(from); + EXPECT_EQ(100, _st_win64_recvfrom(server, buf, sizeof(buf), 0, (sockaddr*)&from, &fromlen)); + EXPECT_EQ(player_addr.sin_port, from.sin_port); + + EXPECT_EQ(100, _st_win64_read(server, buf, sizeof(buf))); + + char a[10], b[20]; + struct iovec two[2] = {{a, sizeof(a)}, {b, sizeof(b)}}; + EXPECT_EQ(30, _st_win64_readv(server, two, 2)); + + memset(buf, 0, sizeof(buf)); + EXPECT_EQ(4, _st_win64_read(server, buf, sizeof(buf))); + EXPECT_STREQ("next", buf); + + EXPECT_EQ(0, _st_win64_close(server)); + EXPECT_EQ(0, _st_win64_close(player)); +} + +// A datagram larger than UDP allows fails at once with EMSGSIZE, which st_sendmsg reports instead of waiting. +VOID TEST(WinIoTest, SendmsgOversizedDatagramFails) +{ + int server, player; + sockaddr_in server_addr, player_addr; + ASSERT_TRUE(win_io_udp_socket(server, server_addr)); + ASSERT_TRUE(win_io_udp_socket(player, player_addr)); + + std::string half(32768, 'x'); + struct iovec iov[2] = {{(void*)half.data(), half.size()}, {(void*)half.data(), half.size()}}; + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_name = &server_addr; + msg.msg_namelen = sizeof(server_addr); + msg.msg_iov = iov; + msg.msg_iovlen = 2; + errno = 0; + EXPECT_EQ(-1, _st_win64_sendmsg(player, &msg, 0)); + EXPECT_EQ(EMSGSIZE, errno); + + EXPECT_EQ(0, _st_win64_close(server)); + EXPECT_EQ(0, _st_win64_close(player)); +} + +// On POSIX, sendmsg and recvmsg work on a TCP stream too, and st_sendmsg is used that way, while Winsock's WSASendMsg +// and WSARecvMsg may take only datagrams, so a stream works the same through io.c. +VOID TEST(WinIoTest, SendmsgRecvmsgStream) +{ + int fds[2]; + ASSERT_TRUE(win_io_tcp_pair(fds)); + + char h[] = "head", p[] = "payload"; + struct iovec out[2] = {{h, 4}, {p, 7}}; + struct msghdr sent; + memset(&sent, 0, sizeof(sent)); + sent.msg_iov = out; + sent.msg_iovlen = 2; + EXPECT_EQ(11, _st_win64_sendmsg(fds[0], &sent, 0)); + + char buf[64]; + std::string got; + while (got.size() < 11) { + struct iovec in = {buf, sizeof(buf)}; + struct msghdr received; + memset(&received, 0, sizeof(received)); + received.msg_iov = ∈ + received.msg_iovlen = 1; + int n = _st_win64_recvmsg(fds[1], &received, 0); + ASSERT_GT(n, 0); + got.append(buf, (size_t)n); + EXPECT_EQ(0, received.msg_flags); + } + EXPECT_EQ("headpayload", got); + + // The end of stream is 0. + EXPECT_EQ(0, _st_win64_close(fds[0])); + struct iovec in = {buf, sizeof(buf)}; + struct msghdr received; + memset(&received, 0, sizeof(received)); + received.msg_iov = ∈ + received.msg_iovlen = 1; + EXPECT_EQ(0, _st_win64_recvmsg(fds[1], &received, 0)); + EXPECT_EQ(0, _st_win64_close(fds[1])); +} + +// close is closesocket, which st_netfd_close calls: the socket is gone afterwards, and closing it again fails. +VOID TEST(WinIoTest, CloseClosesSocket) +{ + SOCKET s = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); + ASSERT_NE(INVALID_SOCKET, s); + int fd = (int)s; + EXPECT_EQ(0, _st_win64_close(fd)); + + int type = 0; + int size = sizeof(type); + EXPECT_EQ(SOCKET_ERROR, ::getsockopt(s, SOL_SOCKET, SO_TYPE, (char*)&type, &size)); + EXPECT_EQ(WSAENOTSOCK, WSAGetLastError()); + + errno = 0; + EXPECT_EQ(-1, _st_win64_close(fd)); + EXPECT_EQ(ENOTSOCK, errno); +} +#endif + +// The test helper st_utest_stream_pair gives two connected stream sockets that ST can wait on: a socketpair on POSIX, +// and a loopback TCP connection on Windows, which has no socketpair. + +// Sends all of the data on a blocking socket. +static bool utest_pair_send_all(int fd, const std::string& data) +{ + size_t sent = 0; + while (sent < data.size()) { + ssize_t n = st_utest_send(fd, data.data() + sent, data.size() - sent); + if (n <= 0) return false; + sent += (size_t)n; + } + return true; +} + +// Receives exactly n bytes from a blocking socket, or less at the end of stream or on error. +static std::string utest_pair_recv_n(int fd, size_t n) +{ + std::string got; + char buf[256]; + while (got.size() < n) { + size_t want = std::min(sizeof(buf), n - got.size()); + ssize_t r = st_utest_recv(fd, buf, want); + if (r <= 0) break; + got.append(buf, (size_t)r); + } + return got; +} + +// Data goes both ways between the two ends, and closing one end is the end of stream at the other. +VOID TEST(UtestPairTest, StreamPairCarriesBothWays) +{ + int fds[2] = {-1, -1}; + ASSERT_EQ(0, st_utest_stream_pair(fds)); + EXPECT_GE(fds[0], 0); + EXPECT_GE(fds[1], 0); + EXPECT_NE(fds[0], fds[1]); + + ASSERT_TRUE(utest_pair_send_all(fds[0], "ping")); + EXPECT_EQ("ping", utest_pair_recv_n(fds[1], 4)); + ASSERT_TRUE(utest_pair_send_all(fds[1], "pong!")); + EXPECT_EQ("pong!", utest_pair_recv_n(fds[0], 5)); + + // More than a socket buffer, in both directions. The ends block and this test is the only reader, so send it in + // pieces that each fit in the buffer: a macOS socketpair holds only 8 KB each way. + std::string big(64 * 1024, 'x'); + for (size_t i = 0; i < big.size(); i++) big[i] = (char)('a' + i % 26); + for (size_t off = 0; off < big.size(); off += 4096) { + std::string piece = big.substr(off, 4096); + ASSERT_TRUE(utest_pair_send_all(fds[0], piece)); + ASSERT_TRUE(piece == utest_pair_recv_n(fds[1], piece.size())) << "offset " << off; + ASSERT_TRUE(utest_pair_send_all(fds[1], piece)); + ASSERT_TRUE(piece == utest_pair_recv_n(fds[0], piece.size())) << "offset " << off; + } + + EXPECT_EQ(0, st_utest_close(fds[0])); + char c; + EXPECT_EQ(0, st_utest_recv(fds[1], &c, 1)); + EXPECT_EQ(0, st_utest_close(fds[1])); +} + +// Many pairs open at once are each connected to their own other end, never to another pair's. +VOID TEST(UtestPairTest, ManyPairsAreEachConnectedToTheirOwnEnd) +{ + const int n = 64; + int fds[n][2]; + for (int i = 0; i < n; i++) { + ASSERT_EQ(0, st_utest_stream_pair(fds[i])) << "pair " << i; + } + + std::vector all; + for (int i = 0; i < n; i++) { + all.push_back(fds[i][0]); + all.push_back(fds[i][1]); + } + std::sort(all.begin(), all.end()); + EXPECT_TRUE(std::unique(all.begin(), all.end()) == all.end()); + + for (int i = 0; i < n; i++) { + char tag[16]; + snprintf(tag, sizeof(tag), "pair-%03d", i); + ASSERT_TRUE(utest_pair_send_all(fds[i][0], tag)); + } + for (int i = 0; i < n; i++) { + char tag[16]; + snprintf(tag, sizeof(tag), "pair-%03d", i); + EXPECT_EQ(std::string(tag), utest_pair_recv_n(fds[i][1], strlen(tag))); + } + + for (int i = 0; i < n; i++) { + EXPECT_EQ(0, st_utest_close(fds[i][0])); + EXPECT_EQ(0, st_utest_close(fds[i][1])); + } +} + +#ifdef _WIN32 // Windows only: the stream pair is a loopback TCP connection +// The two ends are the two sides of one loopback TCP connection, with Nagle off on both, so small writes are not +// delayed, as on a Unix-domain socketpair. Both ends are blocking, as socketpair gives them. +VOID TEST(UtestPairTest, StreamPairIsLoopbackTcpWithoutDelay) +{ + int fds[2] = {-1, -1}; + ASSERT_EQ(0, st_utest_stream_pair(fds)); + + for (int i = 0; i < 2; i++) { + int type = 0; + socklen_t size = sizeof(type); + EXPECT_EQ(0, getsockopt(fds[i], SOL_SOCKET, SO_TYPE, &type, &size)); + EXPECT_EQ(SOCK_STREAM, type); + + int nodelay = 0; + size = sizeof(nodelay); + EXPECT_EQ(0, getsockopt(fds[i], IPPROTO_TCP, TCP_NODELAY, &nodelay, &size)); + EXPECT_NE(0, nodelay) << "end " << i; + } + + // Each end's peer is the other end, on 127.0.0.1. + sockaddr_in local[2], peer[2]; + for (int i = 0; i < 2; i++) { + int size = sizeof(local[i]); + ASSERT_EQ(0, ::getsockname((SOCKET)fds[i], (sockaddr*)&local[i], &size)); + size = sizeof(peer[i]); + ASSERT_EQ(0, ::getpeername((SOCKET)fds[i], (sockaddr*)&peer[i], &size)); + EXPECT_EQ(AF_INET, local[i].sin_family); + EXPECT_EQ(htonl(INADDR_LOOPBACK), local[i].sin_addr.s_addr); + } + EXPECT_EQ(local[0].sin_port, peer[1].sin_port); + EXPECT_EQ(local[1].sin_port, peer[0].sin_port); + EXPECT_EQ(local[0].sin_addr.s_addr, peer[1].sin_addr.s_addr); + EXPECT_EQ(local[1].sin_addr.s_addr, peer[0].sin_addr.s_addr); + + // Blocking: a receive with no data waits instead of failing with WSAEWOULDBLOCK, so it times out. + DWORD timeout = 50; + EXPECT_EQ(0, setsockopt(fds[1], SOL_SOCKET, SO_RCVTIMEO, &timeout, sizeof(timeout))); + char c; + EXPECT_EQ(-1, st_utest_recv(fds[1], &c, 1)); + EXPECT_EQ(WSAETIMEDOUT, WSAGetLastError()); + + EXPECT_EQ(0, st_utest_close(fds[0])); + EXPECT_EQ(0, st_utest_close(fds[1])); +} +#endif + +#ifdef _WIN32 // Windows only: the WSAPoll event system +// Both event system choices use the one WSAPoll backend on Windows. +VOID TEST(WsaPollTest, EventSystemIsWsaPoll) +{ + EXPECT_STREQ("wsapoll", st_get_eventsys_name()); +} + +// Windows retries the SYN after a refusal, so a connect to a port where nothing listens fails after about two +// seconds, longer than ST_UTEST_TIMEOUT of IoConnectTest.ConnectRefusedWhenNobodyListens. WSAPoll did not report a +// failed connect before Windows 10 2004; here it must wake st_connect before its timeout, and the SO_ERROR it reads +// is a POSIX errno, ECONNREFUSED, not the Winsock code. +VOID TEST(WsaPollTest, ConnectRefusedIsReportedBeforeTimeout) +{ + struct sockaddr_in addr; + st_netfd_t listener = io_tcp_listen(addr, 8); + ASSERT_TRUE(listener != NULL); + ASSERT_EQ(0, st_netfd_close(listener)); + + st_netfd_t stfd = io_tcp_socket(AF_INET); + ASSERT_TRUE(stfd != NULL); + StStfdCleanup(stfd); + + st_utime_t timeout = 10 * 1000 * ST_UTIME_MILLISECONDS; + st_utime_t starttime = st_utime(); + errno = 0; + EXPECT_EQ(-1, st_connect(stfd, (sockaddr*)&addr, sizeof(addr), timeout)); + EXPECT_EQ(ECONNREFUSED, errno); + EXPECT_LT(st_utime() - starttime, timeout); +} +#endif diff --git a/utest/st_utest_key.cpp b/utest/st_utest_key.cpp index f1b1532..1645199 100644 --- a/utest/st_utest_key.cpp +++ b/utest/st_utest_key.cpp @@ -5,7 +5,9 @@ #include #include +#ifndef _WIN32 #include +#endif #include #include #include @@ -216,6 +218,7 @@ VOID TEST(KeyTest, InvalidKeyIsRejected) EXPECT_TRUE(st_thread_getspecific(st_key_getlimit()) == NULL); } +#ifndef _WIN32 // POSIX only: an OS thread with pthread struct KeyTestThread { // The keys created, and the one past the limit. std::vector keys_; @@ -263,3 +266,4 @@ VOID TEST(KeyTest, KeyLimitPerThread) EXPECT_EQ(-1, r.create_over_limit_); EXPECT_EQ(EAGAIN, r.errno_over_limit_); } +#endif diff --git a/utest/st_utest_learn_kb.cpp b/utest/st_utest_learn_kb.cpp index 35d0ef5..17e7bc4 100644 --- a/utest/st_utest_learn_kb.cpp +++ b/utest/st_utest_learn_kb.cpp @@ -8,8 +8,10 @@ #include #include +#ifndef _WIN32 #include #include +#endif #define ST_UTIME_MILLISECONDS 1000 #define ST_UTEST_TIMEOUT (100 * ST_UTIME_MILLISECONDS) diff --git a/utest/st_utest_sched.cpp b/utest/st_utest_sched.cpp index e51cae9..1dacc5b 100644 --- a/utest/st_utest_sched.cpp +++ b/utest/st_utest_sched.cpp @@ -5,9 +5,13 @@ #include #include +#ifndef _WIN32 #include +#endif #include +#ifndef _WIN32 #include +#endif #include #include @@ -15,8 +19,10 @@ #include #include +#ifndef _WIN32 #include #include +#endif #define ST_UTIME_MILLISECONDS 1000 #define ST_UTEST_TIMEOUT (100 * ST_UTIME_MILLISECONDS) @@ -1238,6 +1244,7 @@ VOID TEST(InitTest, SecondInitKeepsWaitingCoroutines) EXPECT_EQ('x', r.data_); } +#ifndef _WIN32 // POSIX only: fork, pipes and pthread //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for a program that ends when its last coroutine ends, the classic ST server: main starts the workers, then // calls st_thread_exit instead of returning, and the process exits with status 0 once the last coroutine terminates. @@ -1411,3 +1418,4 @@ VOID TEST(ExitTest, WaitingReaderKeepsProcessAlive) EXPECT_EQ(0, exit_test_run(exit_test_waiting_reader, out)); EXPECT_EQ("got a\n", out); } +#endif diff --git a/utest/st_utest_select.cpp b/utest/st_utest_select.cpp index 56dc337..9325b8e 100644 --- a/utest/st_utest_select.cpp +++ b/utest/st_utest_select.cpp @@ -7,13 +7,17 @@ #include #include #include +#ifndef _WIN32 #include #include +#endif #include #include #include #include +#ifndef _WIN32 #include +#endif #if defined(__APPLE__) #include @@ -21,6 +25,7 @@ #include #endif +#ifndef _WIN32 #include #include #include @@ -28,10 +33,12 @@ #include #include #include +#endif #define ST_UTIME_MILLISECONDS 1000 #define SELECT_TEST_TIMEOUT (1000 * ST_UTIME_MILLISECONDS) +#ifndef _WIN32 // POSIX only: each test runs its own ST in a forked child, with shared memory, pipes and pthread //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for the select event system, the portable one: the only one on Cygwin, and the one an OS thread gets when // it calls st_init without choosing. It watches at most FD_SETSIZE descriptors, so its st_init lowers the descriptor @@ -1448,3 +1455,4 @@ VOID TEST(SelectTest, DestroyFreesEventSystem) EXPECT_GE(r.p_->heap_before_, r.p_->heap_after_ + 3 * sizeof(fd_set)); #endif } +#endif diff --git a/utest/st_utest_stack.cpp b/utest/st_utest_stack.cpp index ce389af..53a04f8 100644 --- a/utest/st_utest_stack.cpp +++ b/utest/st_utest_stack.cpp @@ -4,15 +4,23 @@ #include #include +#ifndef _WIN32 #include +#endif #include +#ifndef _WIN32 #include +#endif #include #include +#ifndef _WIN32 #include +#endif +#ifndef _WIN32 #include #include +#endif #include #define ST_UTIME_MILLISECONDS 1000 @@ -44,6 +52,7 @@ static void* stack_test_record_coroutine(void* arg) return NULL; } +#ifndef _WIN32 // POSIX only: pipes and syscall // Whether the byte at p can be read. write reports EFAULT for an unreadable page instead of faulting. It calls the // system call directly, because ASAN checks the buffer of write, and p may be heap memory just past a stack that // MALLOC_STACK allocated. macOS deprecates syscall, but it still works. @@ -63,6 +72,7 @@ static uintptr_t stack_test_top(const StackTestCoroutine& c) uintptr_t pagesize = (uintptr_t)getpagesize(); return (c.frame_ & ~(pagesize - 1)) + pagesize; } +#endif // Randomization is off by default, and each call returns the previous setting. Locks in current behavior. VOID TEST(RandomizeStacksTest, SwitchReturnsPrevious) @@ -110,6 +120,7 @@ VOID TEST(RandomizeStacksTest, StacksStartAtDifferentOffsets) EXPECT_GT(moved, 0); } +#ifndef _WIN32 // POSIX only: mmap, mprotect interposition with dlsym, pipes // A server turns randomization on while it runs, after some coroutines already exist. One of them exits, and the next // coroutine created frees its stack. The coroutines still running must keep their guard pages, so an overflow of their // stacks still faults. The stacks are mapped next to each other: each new one below the last on Linux, above it on @@ -273,6 +284,7 @@ VOID TEST(RandomizeStacksTest, FreeingRestoresTheGuardPagesCreationProtected) } } } +#endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for describing the main thread's stack to AddressSanitizer. ST runs the main thread as its primordial @@ -291,7 +303,13 @@ extern size_t _st_primordial_stack_size; // The stack of the calling OS thread, from its lowest to its highest address. static void stack_test_thread_stack(char** bottom, char** top) { -#ifdef __APPLE__ +#if defined(_WIN32) + // The whole reserved stack, from the TIB's DeallocationStack up to its StackBase. + ULONG_PTR low = 0, high = 0; + GetCurrentThreadStackLimits(&low, &high); + *bottom = (char*)low; + *top = (char*)high; +#elif defined(__APPLE__) pthread_t self = pthread_self(); *top = (char*)pthread_get_stackaddr_np(self); *bottom = *top - pthread_get_stacksize_np(self); diff --git a/utest/st_utest_sync.cpp b/utest/st_utest_sync.cpp index 376a744..f4296eb 100644 --- a/utest/st_utest_sync.cpp +++ b/utest/st_utest_sync.cpp @@ -6,13 +6,19 @@ #include #include #include +#ifndef _WIN32 #include +#endif #include #include +#ifndef _WIN32 #include +#endif +#ifndef _WIN32 #include #include +#endif #define ST_UTIME_MILLISECONDS 1000 @@ -767,6 +773,7 @@ VOID TEST(SleepTest, NegativeSecondsSleepsUntilStopped) EXPECT_EQ(EINTR, sleeper.errno_); } +#ifndef _WIN32 // POSIX only: fork, shared memory, pipes and pthread //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for a custom clock, such as a simulation or a test tool that drives time itself instead of waiting for it. // st_set_utime_function replaces the monotonic clock behind st_utime, and every timeout counts on it: sleeps, condition @@ -1098,3 +1105,4 @@ VOID TEST(CustomClockTest, RefusedOnceStRuns) EXPECT_EQ(EINVAL, r->null_errno_); EXPECT_EQ(1000 * ST_UTIME_SECONDS, r->utime_after_); } +#endif diff --git a/utest/st_utest_tcp.cpp b/utest/st_utest_tcp.cpp index 54af7d4..5461a90 100644 --- a/utest/st_utest_tcp.cpp +++ b/utest/st_utest_tcp.cpp @@ -5,10 +5,16 @@ #include #include +#include +#include +#include +#ifndef _WIN32 #include +#include #include #include +#endif #define ST_UTIME_MILLISECONDS 1000 #define ST_UTEST_TIMEOUT (100 * ST_UTIME_MILLISECONDS) @@ -105,3 +111,275 @@ VOID TEST(TcpTest, TcpConnection) ST_EXPECT_SUCCESS(r1); } +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +// The utest for an echo server with many concurrent connections, for correctness, not speed. +//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +#define TCP_ECHO_CLIENTS 1000 +#define TCP_ECHO_ROUNDS 3 +#define TCP_ECHO_SIZE 100 +// Generous: connecting 1000 clients at once may overflow the listen backlog, and the OS then retries the SYN after +// about a second (Linux) or two (Windows). A correct run takes far less. +#define TCP_ECHO_TIMEOUT (30 * 1000 * ST_UTIME_MILLISECONDS) +// Small stacks, since there is one coroutine per client and one per accepted connection. +#define TCP_ECHO_STACK (64 * 1024) + +struct TcpEchoTest { + int nn_clients_; + st_netfd_t listener_; + sockaddr_in addr_; + + // The barrier: the server sends each client a go byte only when every client is connected and waits for it, and + // every echo coroutine waits for the client's data, so all the sockets are polled at the same time. + int connected_; + int accepted_; + int nn_ready_; + st_cond_t all_connected_; + + // The results of the echo coroutines. + int nn_echoed_; + int64_t bytes_echoed_; + int echo_errors_; + + TcpEchoTest() : nn_clients_(0), listener_(NULL), connected_(0), accepted_(0), nn_ready_(0), all_connected_(NULL), + nn_echoed_(0), bytes_echoed_(0), echo_errors_(0) { + memset(&addr_, 0, sizeof(addr_)); + } +}; + +struct TcpEchoConn { + TcpEchoTest* t_; + st_netfd_t stfd_; + int id_; +}; + +// The message of one client in one round: its id, the round, and a pattern that depends on both. +static void tcp_echo_message(char* buf, int id, int round) +{ + snprintf(buf, TCP_ECHO_SIZE, "client=%d, round=%d, ", id, round); + for (int i = (int)strlen(buf); i < TCP_ECHO_SIZE; i++) { + buf[i] = (char)('a' + (id * 7 + round * 3 + i) % 26); + } +} + +// Echoes whatever arrives on one accepted connection until the client closes it. +static void* tcp_echo_session(void* arg) +{ + TcpEchoConn* c = (TcpEchoConn*)arg; + TcpEchoTest* t = c->t_; + + char buf[TCP_ECHO_SIZE * 2]; + int64_t bytes = 0; + bool ok = true; + for (;;) { + ssize_t nn = st_read(c->stfd_, buf, sizeof(buf), TCP_ECHO_TIMEOUT); + if (nn == 0) break; + if (nn < 0 || st_write(c->stfd_, buf, nn, TCP_ECHO_TIMEOUT) != nn) { + ok = false; + break; + } + bytes += nn; + } + + if (ok) t->nn_echoed_++; + else t->echo_errors_++; + t->bytes_echoed_ += bytes; + + st_netfd_close(c->stfd_); + delete c; + return NULL; +} + +// Accepts every client and starts an echo coroutine for each, then sends every client the go byte once all are +// connected, and waits for the echo coroutines. +static void* tcp_echo_server(void* arg) +{ + TcpEchoTest* t = (TcpEchoTest*)arg; + std::vector sessions; + std::vector conns; + + for (int i = 0; i < t->nn_clients_; i++) { + st_netfd_t client = st_accept(t->listener_, NULL, NULL, TCP_ECHO_TIMEOUT); + if (!client) break; + + TcpEchoConn* c = new TcpEchoConn(); + c->t_ = t; + c->stfd_ = client; + c->id_ = i; + st_thread_t trd = st_thread_create(tcp_echo_session, c, 1, TCP_ECHO_STACK); + if (!trd) { + st_netfd_close(client); + delete c; + break; + } + sessions.push_back(trd); + conns.push_back(client); + t->accepted_++; + } + int accepted = t->accepted_; + + // Each echo coroutine closes its connection only after its client got the go byte and closed, so a connection + // here is still open when its go byte is sent. Without the go byte, the clients time out, and so do the echo + // coroutines. + int r0 = 0; + while (accepted == t->nn_clients_ && t->connected_ < t->nn_clients_ && r0 == 0) { + r0 = st_cond_timedwait(t->all_connected_, TCP_ECHO_TIMEOUT); + } + int nn_go = 0; + for (size_t i = 0; r0 == 0 && accepted == t->nn_clients_ && i < conns.size(); i++) { + if (st_write(conns[i], "G", 1, TCP_ECHO_TIMEOUT) == 1) nn_go++; + } + + for (size_t i = 0; i < sessions.size(); i++) { + st_thread_join(sessions[i], NULL); + } + + ST_ASSERT_ERROR(accepted != t->nn_clients_, accepted, "Accept every client"); + ST_ASSERT_ERROR(r0, r0, "Wait for every client to connect"); + ST_ASSERT_ERROR(nn_go != t->nn_clients_, nn_go, "Send every go byte"); + return NULL; +} + +// Connects, waits in a read for the go byte, then sends its own messages and checks each echo. +static void* tcp_echo_client(void* arg) +{ + TcpEchoConn* c = (TcpEchoConn*)arg; + TcpEchoTest* t = c->t_; + int id = c->id_; + delete c; + + int fd = -1; + st_netfd_t stfd = NULL; + StFdCleanup(fd, stfd); + + fd = (int)socket(AF_INET, SOCK_STREAM, 0); + ST_ASSERT_ERROR(fd < 0, fd, "Create socket"); + + stfd = st_netfd_open_socket(fd); + ST_ASSERT_ERROR(!stfd, fd, "Open ST socket"); + + int r0 = st_connect(stfd, (const sockaddr*)&t->addr_, sizeof(t->addr_), TCP_ECHO_TIMEOUT); + ST_ASSERT_ERROR(r0, r0, "Connect to server"); + + t->connected_++; + if (t->connected_ == t->nn_clients_) st_cond_signal(t->all_connected_); + + char go = 0; + ssize_t nn = st_read_fully(stfd, &go, 1, TCP_ECHO_TIMEOUT); + ST_ASSERT_ERROR(nn != 1 || go != 'G', (int)nn, "Read the go byte"); + t->nn_ready_++; + + char msg[TCP_ECHO_SIZE], echo[TCP_ECHO_SIZE]; + for (int round = 0; round < TCP_ECHO_ROUNDS; round++) { + tcp_echo_message(msg, id, round); + + // Send the message in two parts, so the server may read it split, and check the echo as a whole. + int half = TCP_ECHO_SIZE / 2; + ST_ASSERT_ERROR(st_write(stfd, msg, half, TCP_ECHO_TIMEOUT) != half, round, "Write first half"); + st_usleep(0); + ST_ASSERT_ERROR(st_write(stfd, msg + half, TCP_ECHO_SIZE - half, TCP_ECHO_TIMEOUT) != TCP_ECHO_SIZE - half, + round, "Write second half"); + + nn = st_read_fully(stfd, echo, TCP_ECHO_SIZE, TCP_ECHO_TIMEOUT); + ST_ASSERT_ERROR(nn != TCP_ECHO_SIZE, (int)nn, "Read the echo"); + ST_ASSERT_ERROR(memcmp(msg, echo, TCP_ECHO_SIZE) != 0, round, "Echo matches the message"); + } + + return NULL; +} + +// How many clients the descriptor limit allows, with room for the other descriptors of the process: each client +// takes two descriptors, its own and the server's end. On POSIX, raise the soft limit toward the hard limit first. +static int tcp_echo_max_clients() +{ +#ifndef _WIN32 + rlim_t want = 2 * TCP_ECHO_CLIENTS + 256; + struct rlimit rlim; + if (getrlimit(RLIMIT_NOFILE, &rlim) < 0) return 0; + + if (rlim.rlim_cur != RLIM_INFINITY && rlim.rlim_cur < want) { + rlim.rlim_cur = (rlim.rlim_max == RLIM_INFINITY || rlim.rlim_max > want) ? want : rlim.rlim_max; + setrlimit(RLIMIT_NOFILE, &rlim); + if (getrlimit(RLIMIT_NOFILE, &rlim) < 0) return 0; + } + if (rlim.rlim_cur != RLIM_INFINITY && rlim.rlim_cur < want) { + return rlim.rlim_cur > 256 ? (int)((rlim.rlim_cur - 256) / 2) : 0; + } +#endif + return TCP_ECHO_CLIENTS; +} + +VOID TEST(TcpTest, EchoManyConcurrentConnections) +{ + TcpEchoTest t; + t.nn_clients_ = tcp_echo_max_clients(); + // Only a system with a very low hard descriptor limit runs fewer clients; Windows, Linux, and macOS run them all. + ASSERT_GT(t.nn_clients_, 0); + if (t.nn_clients_ < TCP_ECHO_CLIENTS) { + printf("The descriptor limit allows only %d of %d echo clients\n", t.nn_clients_, TCP_ECHO_CLIENTS); + } + + // The listener, on a loopback port the OS picks, with the largest backlog. + int fd = (int)socket(AF_INET, SOCK_STREAM, 0); + ASSERT_GE(fd, 0); + t.addr_.sin_family = AF_INET; + t.addr_.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + t.addr_.sin_port = 0; + socklen_t addrlen = sizeof(t.addr_); + if (::bind(fd, (const sockaddr*)&t.addr_, addrlen) != 0 || ::listen(fd, SOMAXCONN) != 0 || + getsockname(fd, (sockaddr*)&t.addr_, &addrlen) != 0) { + st_utest_close(fd); + FAIL() << "Listen on loopback, errno=" << errno; + } + t.listener_ = st_netfd_open_socket(fd); + if (!t.listener_) { + st_utest_close(fd); + FAIL() << "Open ST listener, errno=" << errno; + } + t.all_connected_ = st_cond_new(); + ASSERT_TRUE(t.all_connected_ != NULL); + + // Refresh ST's clock, which an earlier test may leave stale by blocking outside ST. + st_thread_yield(); + + st_thread_t svr = st_thread_create(tcp_echo_server, &t, 1, TCP_ECHO_STACK); + ASSERT_TRUE(svr != NULL); + + std::vector clients; + for (int i = 0; i < t.nn_clients_; i++) { + TcpEchoConn* c = new TcpEchoConn(); + c->t_ = &t; + c->stfd_ = NULL; + c->id_ = i; + st_thread_t trd = st_thread_create(tcp_echo_client, c, 1, TCP_ECHO_STACK); + if (!trd) { + delete c; + break; + } + clients.push_back(trd); + } + EXPECT_EQ(t.nn_clients_, (int)clients.size()); + + int nn_failed = 0; + for (size_t i = 0; i < clients.size(); i++) { + ST_COROUTINE_JOIN(clients[i], r0); + // Print only the first few failures. + if (r0 && nn_failed++ < 5) ADD_FAILURE() << "client " << i << ": " << r0; + } + EXPECT_EQ(0, nn_failed); + + // The server waits for its echo coroutines, which end when their clients close. + ST_COROUTINE_JOIN(svr, r1); + ST_EXPECT_SUCCESS(r1); + + // Every connection was open at the same time, and every byte came back. + EXPECT_EQ(t.nn_clients_, t.connected_); + EXPECT_EQ(t.nn_clients_, t.accepted_); + EXPECT_EQ(t.nn_clients_, t.nn_ready_); + EXPECT_EQ(t.nn_clients_, t.nn_echoed_); + EXPECT_EQ(0, t.echo_errors_); + EXPECT_EQ((int64_t)t.nn_clients_ * TCP_ECHO_ROUNDS * TCP_ECHO_SIZE, t.bytes_echoed_); + + st_cond_destroy(t.all_connected_); + st_netfd_close(t.listener_); +} + diff --git a/utest/st_utest_udp.cpp b/utest/st_utest_udp.cpp index c697e13..0ee1daf 100644 --- a/utest/st_utest_udp.cpp +++ b/utest/st_utest_udp.cpp @@ -6,16 +6,22 @@ #include #include #include +#ifndef _WIN32 #include +#endif #include +#ifndef _WIN32 #include +#endif #include #include +#ifndef _WIN32 #include #include #include #include +#endif #define ST_UTIME_MILLISECONDS 1000 #define ST_UTEST_TIMEOUT (100 * ST_UTIME_MILLISECONDS) @@ -48,12 +54,12 @@ static bool io_udp_socket(IoTestUdpSocket& s) socklen_t addrlen = sizeof(s.addr_); if (::bind(fd, (sockaddr*)&s.addr_, sizeof(s.addr_)) < 0 || getsockname(fd, (sockaddr*)&s.addr_, &addrlen) < 0) { - ::close(fd); + st_utest_close(fd); return false; } s.stfd_ = st_netfd_open_socket(fd); - if (!s.stfd_) ::close(fd); + if (!s.stfd_) st_utest_close(fd); return s.stfd_ != NULL; } @@ -240,6 +246,7 @@ VOID TEST(IoUdpTest, RecvfromFailsWhenPeerRefuses) EXPECT_EQ(ECONNREFUSED, errno); } +#ifndef _WIN32 // POSIX only: signals, fcntl and pthread static volatile sig_atomic_t io_udp_signals = 0; static void io_udp_on_signal(int signo) @@ -314,6 +321,7 @@ VOID TEST(IoUdpTest, RecvfromRetriesWhenSignalInterruptsSystemCall) EXPECT_STREQ("hello", buf); EXPECT_EQ(player.addr_.sin_port, from.sin_port); } +#endif //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// // The utest for st_recvmsg and st_sendmsg: datagram I/O through a msghdr, which adds what st_recvfrom and st_sendto @@ -369,6 +377,7 @@ VOID TEST(IoUdpMsgTest, SendmsgGathersAndRecvmsgScatters) EXPECT_EQ(player.addr_.sin_addr.s_addr, from.sin_addr.s_addr); } +#ifndef _WIN32 // POSIX only: IP_PKTINFO control messages with CMSG // A server bound to every interface must answer from the address the client sent to, or a multi-homed host replies // from the wrong address. With IP_PKTINFO on, st_recvmsg delivers each datagram's destination address as a control // message. Locks in current behavior. @@ -416,6 +425,7 @@ VOID TEST(IoUdpMsgTest, RecvmsgReportsDestinationAddress) memcpy(&info, CMSG_DATA(cmsg), sizeof(info)); EXPECT_EQ(htonl(INADDR_LOOPBACK), info.ipi_addr.s_addr); } +#endif // st_recvfrom drops the tail of a datagram larger than the buffer without saying so. st_recvmsg says so: it returns // the bytes that fit and sets MSG_TRUNC in msg_flags, so a server can count or reject oversized packets. Locks in @@ -538,6 +548,7 @@ VOID TEST(IoUdpMsgTest, RecvmsgFailsWhenPeerRefuses) EXPECT_EQ(ECONNREFUSED, errno); } +#ifndef _WIN32 // POSIX only: signals, fcntl and pthread // The same signal interrupts recvmsg. st_recvmsg retries it, and the retry returns the datagram the player sends // later, with the player's address in msg_name. The test clears O_NONBLOCK so recvmsg waits in the kernel, as for // st_recvfrom. Locks in current behavior. @@ -587,6 +598,7 @@ VOID TEST(IoUdpMsgTest, RecvmsgRetriesWhenSignalInterruptsSystemCall) EXPECT_STREQ("hello", buf); EXPECT_EQ(player.addr_.sin_port, from.sin_port); } +#endif // The buffers of one datagram add up to more than UDP allows: st_sendmsg fails at once with EMSGSIZE instead of // waiting for the socket to become writable. Locks in current behavior. @@ -628,6 +640,7 @@ static void* io_consume_all_coroutine(void* arg) } #endif +#ifndef _WIN32 // POSIX only: Unix-domain datagram socket pairs // Loopback UDP never runs out of send buffer, so this uses a local datagram socket pair, whose sender can't send // while its queued datagrams fill the send buffer, like a producer that outruns its consumer. On Linux, the kernel // reports the socket as not ready: st_sendmsg fails with ETIME when nobody reads, and waits until the consumer catches @@ -718,3 +731,4 @@ VOID TEST(IoUdpTest, SendtoWaitsForFullQueue) EXPECT_EQ(ENOBUFS, errno); #endif } +#endif