Skip to content

Commit b3fce39

Browse files
authored
fix: Make NativeServer link and run on Windows, unblocking Windows Native CI (#641)
**Description** uni's Scala Native test binary cannot link on Windows — at all, today, on `main`: ``` error LNK2019: unresolved external symbol poll error LNK2019: unresolved external symbol scalanative_pollerr error LNK2019: unresolved external symbol scalanative_pollhup error LNK2019: unresolved external symbol scalanative_pollin error LNK2019: unresolved external symbol scalanative_pollnval referenced in function ...wvlet.uni.http.NativeServerTest... fatal error LNK1120: 5 unresolved externals ``` Scala Native's [`posixlib/poll.c`](https://github.com/scala-native/scala-native/blob/v0.5.12/posixlib/src/main/resources/scala-native/poll.c) wraps its whole body in `#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))`, so `poll` and the `scalanative_poll*` constant accessors don't exist there. `NativeSocket` referenced `scalanative.posix.poll`, every native HTTP test starts a server, and so the whole binary was unlinkable. That gap is why uni had **no Windows Scala Native CI**, and therefore why #640's root cause — v2026.1.17 shipping a POSIX-only `#include <dlfcn.h>` in `uni_curl_shim.c` — broke every downstream Windows build instead of failing here. **The fix.** Exactly three calls differ on Windows. Everything else uni uses (`socket`, `bind`, `listen`, `accept`, `recv`, `send`, `setsockopt`, `shutdown`) posixlib already maps onto winsock — the failed link proved it, by resolving all of them and only missing `poll`. So `uni_socket_shim.c` takes those three, behind an `@extern object SocketShim`: | | POSIX | Windows | |---|---|---| | `uni_socket_startup()` | nothing | `WSAStartup`, once, via `InitOnceExecuteOnce` | | `uni_socket_wait_readable(fd, ms)` | `poll` | `WSAPoll` | | `uni_socket_close(fd)` | `close` | `closesocket` | Three things here are easy to get wrong, so they're in the [ADR](https://github.com/wvlet/uni/blob/fix/native-server-windows-poll/adr/2026-07-08-native-socket-shim.md): - **The split has to be in C.** Scala Native has no per-OS source directory (`.native` is per-*platform*), and DCE keeps every *reachable* branch — so a runtime `if (isWindows)` in Scala links both sides and still fails on `scalanative_pollin`. Merely *referencing* `posix.poll` is the break. (`scalanative.windows.WinSocketApi.WSAPoll` is the mirror-image problem: referencing it breaks the POSIX link on `ws2_32`.) - **`#pragma comment(lib, "ws2_32.lib")`, not `@link("ws2_32")`.** Scala Native compiles this `.c` into every downstream binary, including ones that never open a socket. A bare `WSAPoll` reference with no guaranteed `-lws2_32` breaks those links — precisely the #622 trap. The pragma embeds the dependency in the object's linker directives, so it travels with the object; a Scala-level `@link` gets dropped by DCE. Scala Native's own `posixlib/sys/socket.c` does exactly this. - **`WSAStartup` had no other caller.** Winsock rejects every `socket()` with `WSANOTINITIALISED` until it runs. Scala Native calls it from `WinSocketApiOps.init()`, reached only by javalib's `java.net` — which uni's posixlib sockets never touch. And `close()` on a socket *links* on Windows (`oldnames.lib` → `_close`) but only knows CRT file descriptors, so it silently leaks the socket; that applies to the error-cleanup paths in `bindAndListen`/`connect` too, not just the public `close`. **Verification.** - The shim's contract is exercised directly by a C harness over a `socketpair`: idle → timeout, data pending → readable, drained → timeout, **data+hangup → readable** (drain before reporting hangup), hangup-only → readable/EOF, closed fd → error. All pass, and `uni_socket_startup()` is idempotent. - `projectNative/test` passes locally on macOS: 65/65, including `NativeWebSocketClientTest`'s heartbeat test, which is the poll-timeout path. - CI now runs a real **`Scala Native (Windows)`** job — restored in place of #640's standalone `clang` compile, which was only ever a stand-in for this. It exercises `uni_socket_shim.c` (`WSAStartup`/`WSAPoll`/`closesocket`) *and* `uni_curl_shim.c` (`GetProcAddress` over `EnumProcessModules`) at runtime, not just at compile time. Gated as before: every push to `main`, plus PRs touching native code. `check-curl-shim.sh` stays as a step in the Linux Native job. It is still irreplaceable: `build.sbt` passes `-lcurl` unconditionally, so no Scala Native job on any OS can notice the curl shim regrowing a libcurl symbol reference — only a consumer without `-lcurl` breaks. Its Windows/`llvm-nm` branch is dropped now that the real job covers Windows compilation. **Related Issue/Task** Follow-up to #640. Removes the "a real Windows Native job is impossible" caveat that PR had to record, and closes uni's Windows Scala Native coverage gap at its source. **Checklist** - [x] This pull request focuses on a single task. - [x] The change does not contain security credentials 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 319de15 commit b3fce39

7 files changed

Lines changed: 481 additions & 115 deletions

File tree

.github/scripts/check-curl-shim.sh

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,17 @@
11
#!/usr/bin/env bash
22
#
3-
# Guards uni_curl_shim.c, which Scala Native compiles into every downstream binary on every platform
4-
# it supports. Two things can go wrong, and both have:
3+
# Asserts that uni_curl_shim.c compiles to an object referencing no libcurl symbol.
54
#
6-
# 1. It fails to compile. v2026.1.17 included <dlfcn.h>, which the MSVC toolchain does not ship,
7-
# breaking every downstream Windows Scala Native build.
8-
# 2. Its object references a libcurl symbol, which breaks the link of downstream projects that
9-
# never pull in -lcurl (issue #622, adr/2026-07-06-curl-shim-weak-linking.md).
5+
# Scala Native compiles that file into every downstream binary, whether or not the project uses
6+
# CurlBindings. If its object names `curl_easy_setopt`, a project that never links libcurl fails to
7+
# link (issue #622, adr/2026-07-06-curl-shim-weak-linking.md).
108
#
11-
# No Scala Native job in this repo catches (2): build.sbt passes -lcurl unconditionally, so every uni
12-
# native binary resolves those symbols and links happily. Only a consumer without -lcurl breaks, and
13-
# inspecting the object directly is what stands in for that consumer.
9+
# No Scala Native job in this repo can catch that, on any OS: build.sbt passes -lcurl unconditionally,
10+
# so every uni native binary resolves those symbols and links happily. Only a consumer without -lcurl
11+
# breaks. Inspecting the object directly is what stands in for that consumer.
1412
#
15-
# Nor can one catch (1) on Windows: uni's NativeServer uses POSIX poll(), which Scala Native's
16-
# posixlib only builds on unix/Apple, so uni's native test binary cannot link on Windows at all.
17-
# Compiling this one file standalone with clang is the coverage that is available there.
13+
# The other way this file has broken — failing to compile at all, as when v2026.1.17 included the
14+
# POSIX-only <dlfcn.h> — is covered by the "Scala Native (Windows)" job, which builds it for real.
1815
set -euo pipefail
1916

2017
shim="uni/.native/src/main/resources/scala-native/uni_curl_shim.c"
@@ -24,12 +21,7 @@ echo "== Compiling ${shim} with $(clang --version | head -1)"
2421
clang -c "${shim}" -o "${obj}" -Wall -Wextra -Werror
2522

2623
echo "== Undefined symbols"
27-
if [[ "${RUNNER_OS:-}" == "Windows" ]]; then
28-
nm_undefined=(llvm-nm --undefined-only)
29-
else
30-
nm_undefined=(nm -u)
31-
fi
32-
"${nm_undefined[@]}" "${obj}" | tee "${obj}.undefined"
24+
nm -u "${obj}" | tee "${obj}.undefined"
3325

3426
if grep -qi 'curl_easy' "${obj}.undefined"; then
3527
echo

.github/workflows/test.yml

Lines changed: 83 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -137,25 +137,95 @@ jobs:
137137
check_name: Test Report Scala Native
138138
annotate_only: true
139139
detailed_summary: true
140-
curl_shim_c_windows:
141-
# Scala Native compiles every .c under resources/scala-native/ from every jar on the classpath,
142-
# so uni's uni_curl_shim.c lands in each downstream Windows binary — including ones that never
143-
# call curl. Nothing else here compiles that file for Windows, which is how v2026.1.17 shipped a
144-
# POSIX-only `#include <dlfcn.h>` and broke those consumers rather than this repo's CI.
145-
#
146-
# A full Windows Scala Native job would be the broader guard, but uni cannot run one: NativeServer
147-
# uses POSIX poll(), and Scala Native's posixlib only builds poll.c on unix/Apple, so uni's native
148-
# test binary fails to link on Windows with `unresolved external symbol scalanative_pollin`.
149-
# Compiling this one file with the same clang/MSVC toolchain is the coverage available today.
150-
name: curl shim C (Windows)
140+
test_native_3_windows:
141+
# Scala Native compiles every .c under resources/scala-native/ from every jar on the classpath, so
142+
# uni's C (uni_curl_shim.c, uni_socket_shim.c) lands in each downstream Windows binary — including
143+
# ones that never call curl or open a socket. uni had no Windows Native build, so v2026.1.17
144+
# shipped a POSIX-only `#include <dlfcn.h>` and broke those consumers rather than this repo's CI.
145+
name: Scala Native (Windows)
151146
needs: changes
147+
# ~15 minutes, nearly all of it toolchain setup, so don't spend it on every PR: run it on every
148+
# push to main, and on the pull requests that can actually break it — those touching native code.
152149
if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.native == 'true' }}
153150
runs-on: windows-latest
154151
steps:
155152
- uses: actions/checkout@v7
156-
- name: Compile the shim and check for libcurl symbol references
153+
- uses: actions/setup-java@v5
154+
with:
155+
distribution: 'temurin'
156+
java-version: '23'
157+
- uses: ilammy/msvc-dev-cmd@v1
158+
- name: Install LLVM
159+
run: choco install llvm -y
160+
shell: pwsh
161+
- name: Cache vcpkg artifacts
162+
uses: actions/cache@v4
163+
id: vcpkg-cache
164+
with:
165+
path: C:\vcpkg\installed
166+
key: vcpkg-windows-x64-curl-zlib-openssl-v2
167+
- name: Install libcurl, zlib and OpenSSL
168+
if: steps.vcpkg-cache.outputs.cache-hit != 'true'
169+
run: vcpkg install curl:x64-windows zlib:x64-windows openssl:x64-windows
170+
shell: pwsh
171+
- name: Expose the native libraries to the compiler, the linker and the test binary
172+
# Each `-lfoo` / @link("foo") reaches clang in MSVC mode as a request for `foo.lib`:
173+
# `-lcurl -lz` from this build, and `-lcrypto -lssl -lzlib` from Scala Native's own javalib
174+
# (zlib under both names). vcpkg installs the import libraries under their upstream names,
175+
# which vary by port revision, so for each name asked for take the first candidate that exists.
176+
run: |
177+
$root = "$env:VCPKG_INSTALLATION_ROOT\installed\x64-windows"
178+
$libDir = "$root\lib"
179+
$wanted = [ordered]@{
180+
"curl.lib" = @("libcurl.lib")
181+
"crypto.lib" = @("libcrypto.lib")
182+
"ssl.lib" = @("libssl.lib")
183+
"z.lib" = @("zlib.lib", "zlib1.lib", "libz.lib")
184+
"zlib.lib" = @("z.lib", "zlib1.lib", "libz.lib")
185+
}
186+
foreach ($dst in $wanted.Keys) {
187+
$dstPath = Join-Path $libDir $dst
188+
if (Test-Path $dstPath) { continue }
189+
foreach ($src in $wanted[$dst]) {
190+
$srcPath = Join-Path $libDir $src
191+
if (Test-Path $srcPath) {
192+
Write-Host "Copying $src -> $dst"
193+
Copy-Item $srcPath $dstPath -Force
194+
break
195+
}
196+
}
197+
if (-not (Test-Path $dstPath)) { Write-Host "WARNING: no candidate found for $dst" }
198+
}
199+
Get-ChildItem $libDir -Filter *.lib | Select-Object -ExpandProperty Name | Sort-Object
200+
# Scala Native's own javalib ships a z.c that includes <zlib.h>, so the headers must be
201+
# reachable too, not just the import libraries.
202+
echo "C_INCLUDE_PATH=$root\include" >> $env:GITHUB_ENV
203+
echo "INCLUDE=$root\include;$env:INCLUDE" >> $env:GITHUB_ENV
204+
# LIB is where lld-link searches; bin is where the linked test binary finds libcurl.dll.
205+
echo "LIB=$libDir;$env:LIB" >> $env:GITHUB_ENV
206+
echo "$root\bin" >> $env:GITHUB_PATH
207+
shell: pwsh
208+
# `testOnly` still links the whole native test binary, so both of uni's C shims get compiled and
209+
# linked for Windows here; running the http suite then exercises uni_socket_shim.c (WSAStartup /
210+
# WSAPoll / closesocket) and uni_curl_shim.c (GetProcAddress over EnumProcessModules) for real.
211+
#
212+
# Deliberately not the full `projectNative/test` yet. Six tests outside http fail on Windows, all
213+
# of them predating this job and none socket-related: IOSymlinkTest (3 — uni itself raises
214+
# `createSymlink is not supported on Scala Native + Windows`, so they want skipping there),
215+
# IOPathTest's `resolve child path` (a Windows-naive expectation), and JSONTest /
216+
# YAMLFormatterTest (their `stripMargin` literals arrive CRLF, as the repo has no .gitattributes,
217+
# while the formatters emit LF). Fix those, then widen this step to projectNative/test.
218+
- name: Scala Native test (http)
219+
run: JVM_OPTS=-Xmx4g ./sbt "uniNative/testOnly wvlet.uni.http.*"
157220
shell: bash
158-
run: .github/scripts/check-curl-shim.sh
221+
- name: Publish Test Report
222+
uses: mikepenz/action-junit-report@v6
223+
if: always() # always run even if the previous step fails
224+
with:
225+
report_paths: '**/target/test-reports/TEST-*.xml'
226+
check_name: Test Report Scala Native (Windows)
227+
annotate_only: true
228+
detailed_summary: true
159229
package_src:
160230
name: Verify packageSrc
161231
needs: changes

adr/2026-07-06-curl-shim-weak-linking.md

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -68,12 +68,16 @@ on stderr and `abort()`s rather than jumping to NULL.
6868
Two CI additions guard this, because the file's two failure modes need two
6969
different checks:
7070

71-
- **`curl_shim_c_windows`** ("curl shim C (Windows)") compiles the file standalone
72-
with clang on `windows-latest`. Nothing else here compiles the Windows half of
73-
the `#if`, which is how v2026.1.17's POSIX-only `#include <dlfcn.h>` shipped. It
74-
runs on every push to `main` and on pull requests touching native code (the
75-
`native` paths-filter): a `.c` file, anything under a `.native/` source tree, or
76-
the Scala Native version in `project/`.
71+
- **`test_native_3_windows`** ("Scala Native (Windows)") builds and runs the native
72+
test suite on Windows, compiling this shim for real. Nothing here compiled the
73+
Windows half of the `#if` when v2026.1.17 shipped its POSIX-only
74+
`#include <dlfcn.h>`; uni had no Windows Native build at all, because
75+
`NativeServer` could not link there — see
76+
[`2026-07-08-native-socket-shim.md`](2026-07-08-native-socket-shim.md), which
77+
fixed that and made this job possible. ~15 minutes, so it runs on every push to
78+
`main` and only on pull requests touching native code (the `native`
79+
paths-filter): a `.c` file, anything under a `.native/` source tree, or the Scala
80+
Native version in `project/`.
7781
- **`.github/scripts/check-curl-shim.sh`**, a step in the Linux Native job, which
7882
compiles the shim standalone and asserts via `nm -u` that the object names no
7983
`curl_easy_*` symbol. No Scala Native job can catch that regression on any OS:
@@ -113,27 +117,6 @@ separate — but Scala Native's own `nativelib` already links both, so the shim
113117
inherits them. No extra linker option is required from consumers. Verified by
114118
inspecting Scala Native's link line (`[pthread, dl, m, crypto, curl, z]`).
115119

116-
### A real "Scala Native on Windows" CI job would be better, and is not possible yet
117-
118-
The obvious guard — build and run the native test suite on Windows, which would
119-
cover this break and any future one — was tried and abandoned. It gets the whole
120-
toolchain up (LLVM, vcpkg libcurl/zlib/OpenSSL, aliasing each `foo.lib` that
121-
`-lfoo` asks for), compiles every source *including this shim*, and then fails at
122-
the final link:
123-
124-
error LNK2019: unresolved external symbol scalanative_pollin
125-
referenced in function ...wvlet.uni.http.NativeServerTest...
126-
fatal error LNK1120: 5 unresolved externals
127-
128-
`NativeServer` uses POSIX `poll()`, and Scala Native's `posixlib/poll.c` is wrapped
129-
in `#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))` — the
130-
symbols do not exist on Windows. Every native HTTP test spins up a server, so uni's
131-
native test binary cannot link there at all. Giving `NativeServer` a Windows path
132-
(`WSAPoll`) would unblock it; until then, compiling the shim standalone is the
133-
Windows coverage available. Notably, that abandoned run *did* prove the shim
134-
compiles and links clean under clang/MSVC: none of the unresolved symbols were
135-
`curl_easy_*`.
136-
137120
### Windows: why module enumeration, and why `PSAPI_VERSION 2`
138121

139122
Windows has no `RTLD_DEFAULT`. `GetProcAddress` takes one `HMODULE` at a time,
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# 2026-07-08: `uni_socket_shim.c` carries the Windows socket differences, in C
2+
3+
## Context
4+
5+
uni's Native HTTP server and WebSocket support (`NativeHttpServer`, `NativeWebSocket`,
6+
`NativeSocket`) are built directly on Scala Native's posixlib sockets. On
7+
Windows that binary would not even link:
8+
9+
error LNK2019: unresolved external symbol poll
10+
error LNK2019: unresolved external symbol scalanative_pollin
11+
referenced in function ...wvlet.uni.http.NativeServerTest...
12+
fatal error LNK1120: 5 unresolved externals
13+
14+
Scala Native's `posixlib/poll.c` wraps its whole body in
15+
`#if defined(__unix__) || defined(__unix) || defined(unix) || (defined(__APPLE__) && defined(__MACH__))`,
16+
so `poll` and the `scalanative_poll*` constant accessors do not exist on Windows.
17+
That single gap made uni's entire native test binary unlinkable there — which is
18+
why uni had no Windows Scala Native CI at all, and therefore why v2026.1.17 could
19+
ship a POSIX-only `#include <dlfcn.h>` in `uni_curl_shim.c` and break every
20+
downstream Windows build instead of failing here (see
21+
[`2026-07-06-curl-shim-weak-linking.md`](2026-07-06-curl-shim-weak-linking.md)).
22+
23+
Three calls differ on Windows. Everything else uni uses — `socket`, `bind`,
24+
`listen`, `accept`, `recv`, `send`, `setsockopt`, `shutdown` — posixlib already
25+
maps onto winsock, and the failed link proved it: `poll` was the *only* thing
26+
missing.
27+
28+
## Decision
29+
30+
Put all three behind fixed-arity C functions in
31+
`uni/.native/src/main/resources/scala-native/uni_socket_shim.c`, called from an
32+
`@extern object SocketShim`:
33+
34+
| | POSIX | Windows |
35+
|---|---|---|
36+
| `uni_socket_startup()` | nothing | `WSAStartup`, once, via `InitOnceExecuteOnce` |
37+
| `uni_socket_wait_readable(fd, ms)` | `poll` | `WSAPoll` |
38+
| `uni_socket_close(fd)` | `close` | `closesocket` |
39+
40+
## Non-obvious points a future reader would otherwise reverse-engineer
41+
42+
### The OS split has to be in C — Scala cannot express it
43+
44+
Scala Native has no per-OS source directory (`.native` is per-*platform*, not
45+
per-OS), and dead-code elimination keeps every *reachable* branch. So a runtime
46+
`if (Platform.isWindows) ... else scalanative.posix.poll.poll(...)` in Scala
47+
still links both sides, and still fails on `scalanative_pollin`. Merely
48+
*referencing* `scala.scalanative.posix.poll` is what breaks the build. The branch
49+
must happen at C preprocessing time.
50+
51+
`scala.scalanative.windows.WinSocketApi` does expose `WSAPoll`, but it is the
52+
mirror image of the same problem: referencing it from shared Scala makes the
53+
POSIX link fail on `ws2_32`.
54+
55+
### Why the whole readable-wait contract lives in C, not just the `poll` call
56+
57+
POSIX `struct pollfd.fd` is an `int`; Windows `WSAPOLLFD.fd` is a `SOCKET`, a
58+
64-bit `UINT_PTR` on Win64. One Scala `CStruct3` binding cannot describe both
59+
layouts. Returning a plain `int` verdict (1 readable / 0 timeout / -1 error)
60+
keeps the struct on the C side entirely. The EINTR retry and the `revents`
61+
interpretation moved along with it, since they are meaningless to split.
62+
63+
### `#pragma comment(lib, "ws2_32.lib")` — not `@link("ws2_32")`
64+
65+
Scala Native compiles this `.c` into **every** downstream binary, including ones
66+
that never open a socket. A bare `WSAPoll` reference with no guaranteed
67+
`-lws2_32` would break those links — exactly the trap `uni_curl_shim.c` fell into
68+
in #622. A `#pragma comment(lib, ...)` embeds the dependency in the object file's
69+
linker directives, so it travels with the object and always applies; a
70+
Scala-level `@link("ws2_32")` would be dropped by DCE when `CurlBindings`-style
71+
reachability fails. Scala Native's own `posixlib/sys/socket.c` uses the same
72+
pragma. MSVC-family linkers only, which is what Scala Native's Windows support
73+
targets.
74+
75+
### Windows needs `WSAStartup`, and nothing else was going to call it
76+
77+
Winsock rejects every `socket()` with `WSANOTINITIALISED` until `WSAStartup` has
78+
run in the process. Scala Native calls it from `WinSocketApiOps.init()`, reached
79+
only by javalib's `java.net` implementations — which uni's posixlib-based sockets
80+
never touch. `NativeSocket.bindAndListen` and `.connect` therefore call
81+
`ensureStarted()` first. The C side is `InitOnceExecuteOnce`-guarded and
82+
idempotent, so callers need not track it. There is no matching `WSACleanup`: the
83+
sockets live as long as the process.
84+
85+
### `close(fd)` on a Windows socket silently does the wrong thing
86+
87+
`close` *links* on Windows — `oldnames.lib` aliases it to the CRT's `_close`
88+
but `_close` operates on CRT file descriptors, not socket handles. It would fail
89+
with `EBADF` and leak the socket. Sockets must be closed with `closesocket`. Note
90+
this applies to the error-cleanup paths in `bindAndListen` / `connect` too, not
91+
only the public `close`.
92+
93+
### `uni_socket_wait_readable` must be annotated `@blocking`
94+
95+
`scala.scalanative.unsafe.blocking` marks an `extern` that may park the calling
96+
thread, so Scala Native leaves it at a GC safepoint for the duration. Scala
97+
Native annotates its own `posix.poll.poll` that way. Dropping the annotation while
98+
moving the call into this shim does not fail to compile and does not fail on
99+
macOS — it makes the collector wait on a thread sitting inside `poll`, until:
100+
101+
[ScalaNative GC|Warning] Waiting for 1 thread(s) to reach safepoint (60.0s elapsed)
102+
[ScalaNative GC|Error] FATAL: Timeout after 60.0s waiting for 1 thread(s)
103+
Test runner interrupted by fatal signal 6
104+
105+
Any future blocking call added here needs the same annotation. `uni_socket_close`
106+
and `uni_socket_startup` do not: neither parks the thread.
107+
108+
### Narrowing `SOCKET` to `int` is safe
109+
110+
Scala Native stores a socket as `CInt`, having narrowed the `SOCKET` winsock
111+
returned. Widening it back is sound: Windows keeps socket handles inside the low
112+
32 bits precisely so they can be passed through `int` for interoperability. Cast
113+
through `unsigned int``(SOCKET)(unsigned int)fd` — not straight to `UINT_PTR`:
114+
a signed `int` with bit 31 set would sign-extend to `0xFFFFFFFF........` and hand
115+
winsock an invalid `SOCKET`.
116+
117+
### `POLLIN` still works on Windows
118+
119+
`WSAPoll` only ever sets `POLLRDNORM` in `revents`, but `winsock2.h` defines
120+
`POLLIN` as `(POLLRDNORM | POLLRDBAND)`, so testing `revents & POLLIN` reads the
121+
same on both platforms. `winsock2.h` also supplies `POLLERR`, `POLLHUP` and
122+
`POLLNVAL`, so the verdict logic is shared, not duplicated.
123+
124+
## Consequences
125+
126+
- uni's Scala Native test binary links and runs on Windows, so CI gained a real
127+
`Scala Native (Windows)` job. That job — not a standalone `clang` compile — is
128+
now what would have caught v2026.1.17's `<dlfcn.h>` regression, and it exercises
129+
both shims at runtime. `NativeServerTest` passes 21/21 there, keep-alive and
130+
read timeouts (the `WSAPoll` timeout paths) and WebSocket upgrade included.
131+
- That job runs `uniNative/testOnly wvlet.uni.http.*`, not the full
132+
`projectNative/test`. `testOnly` still links the entire test binary, so both
133+
shims are compiled and linked for Windows either way. Six tests outside `http`
134+
fail there, all predating the job and none socket-related — they are what stands
135+
between this and the full suite:
136+
- `IOSymlinkTest` (3): uni raises `createSymlink is not supported on Scala Native
137+
+ Windows`; the tests need skipping on that platform.
138+
- `IOPathTest.resolve child path`: the expectation is Windows-naive.
139+
- `JSONTest` / `YAMLFormatterTest`: their `stripMargin` literals arrive CRLF (the
140+
repo has no `.gitattributes`, and `actions/checkout` leaves Windows'
141+
`core.autocrlf=true`), while the formatters emit LF. `* text=auto eol=lf` would
142+
fix both.
143+
- `NativeSocket` no longer imports `scalanative.posix.poll` or
144+
`scalanative.posix.unistd`. Re-adding either would re-break Windows; the CI job
145+
is what notices.
146+
- Error *messages* on Windows are still POSIX-shaped: winsock reports failures via
147+
`WSAGetLastError()`, not `errno`, so a failed `bind` yields uni's generic
148+
message rather than a specific cause. uni never read `errno` here, so nothing
149+
regressed — but a future "why did bind fail" improvement needs a Windows branch.
150+
- `WSAPoll` before Windows 10 2004 does not report a failed connection attempt in
151+
`revents`. uni only polls accepted/connected sockets for readability, so this
152+
does not bite; do not extend the shim to poll for connect-completion without
153+
revisiting it.

0 commit comments

Comments
 (0)