Skip to content

Commit a88aff6

Browse files
authored
fix: Keep uni_curl_shim.c buildable on Windows so downstream Native builds link (#640)
**Description** uni 2026.1.17 broke every downstream Scala Native build on Windows: ``` uni_native0.5_3-2026.1.17-6/scala-native/uni_curl_shim.c:44:10: fatal error: 'dlfcn.h' file not found ``` The `dlsym` rewrite that fixed #622 reached for `<dlfcn.h>` and `<pthread.h>` unconditionally, and MSVC ships neither. Because Scala Native compiles every `.c` under `resources/scala-native/` from every jar on the classpath, this hits consumers that never touch curl — [wvlet's Windows native build](https://github.com/wvlet/wvlet/actions/runs/28837314411) is failing on `main` for exactly this reason, and it only links `wvc-lib`. Both Windows jobs there die on this one error and nothing else. **The fix.** Split the two POSIX primitives per platform, preserving the property #622 turned on — *the object file must reference no libcurl symbol*, or downstream links without `-lcurl` fail: | | POSIX | Windows | |---|---|---| | lookup | `dlsym(RTLD_DEFAULT, name)` | `GetProcAddress` over `EnumProcessModules` | | run-once | `pthread_once` | `InitOnceExecuteOnce` | Windows has no all-modules handle, so the shim enumerates the process's loaded modules and tries each — the process image first, matching `dlsym`'s order. `#define PSAPI_VERSION 2` keeps `EnumProcessModules` in `kernel32` (as `K32EnumProcessModules`) rather than `psapi.lib`, since a jar-resource `.c` can't make downstream binaries add a linker flag. The POSIX path is unchanged in behaviour. One consequence worth flagging: runtime lookup only finds *exported* symbols, so **libcurl must be linked as a DLL**, not a static `.lib`. vcpkg's default `x64-windows` / `arm64-windows` triplets already do that. This isn't new to Windows — a static `libcurl.a` is equally invisible to `dlsym(RTLD_DEFAULT, ...)`. It's the price #622's fix already paid; the ADR and the user docs now say so out loud. **Why CI didn't catch it, and what now does.** Two distinct failure modes, neither check subsuming the other: 1. **`curl shim C (Windows)`** — compiles the shim standalone with clang on `windows-latest`. Nothing else here compiles the Windows half of the `#if`. 2. **`check-curl-shim.sh`** — one step in the Linux Native job, asserting via `nm -u` that the object names no `curl_easy_*` symbol. *No* Scala Native job can catch that on any OS: [`build.sbt` passes `-lcurl` unconditionally](https://github.com/wvlet/uni/blob/main/build.sbt#L101-L103), so uni's own binaries always resolve those symbols and link happily. Only a consumer that doesn't link libcurl breaks — which is precisely how #622 escaped this repo's CI. Inspecting the object stands in for that consumer. I checked (2) bites rather than rubber-stamps: against `v2026.1.16`'s `extern`-based shim it reports `_curl_easy_setopt` / `_curl_easy_getinfo` and exits 1, reproducing #622. Both are gated on a new narrow `native` paths-filter (`**.c`, `**/.native/**`, `project/**`, the workflow itself), so they run on every push to `main` and only on PRs that can actually break them. (`dorny/paths-filter` matches with `dot: true`, so `**/.native/**` does reach the dot-prefixed source trees.) The `changes` filter never watched `**.c` at all before, so an edit touching only this file skipped CI outright. > [!IMPORTANT] > **A real `Scala Native (Windows)` job would be the better guard, and uni cannot run one.** I built it — LLVM, vcpkg libcurl/zlib/OpenSSL, aliasing every `foo.lib` that `-lfoo` asks for — and it compiles everything, then fails at the final link: > > ``` > error LNK2019: unresolved external symbol scalanative_pollin > referenced in function ...wvlet.uni.http.NativeServerTest... > fatal error LNK1120: 5 unresolved externals > ``` > > `NativeServer` uses POSIX `poll()`, and 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) is wrapped in `#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__))` — those symbols don't exist on Windows. Every native HTTP test starts a server, so uni's native test binary is unlinkable there, full stop. Giving `NativeServer` a `WSAPoll` path would unblock it; this PR's history has the working toolchain setup to reuse. Worth filing as a follow-up issue. > > That discarded run wasn't wasted: it linked everything *except* `poll`, so none of the unresolved symbols were `curl_easy_*` — proving the shim compiles **and links** clean under clang/MSVC, which is the regression this PR is about. **Docs.** The old text said only *"Scala Native requires libcurl to be available at runtime."* Added [Linking libcurl on Scala Native](https://github.com/wvlet/uni/blob/fix/curl-shim-windows/docs/http/client.md#linking-libcurl-on-scala-native) covering the three things a Native user actually hits: you need libcurl *only* if you use the HTTP client (DCE drops `@link("curl")` otherwise — that's what #622 bought, and nobody would guess it); it must be a shared library; and on Windows you alias `libcurl.lib` to the `curl.lib` Scala Native asks for. **Related Issue/Task** Follow-up to #622 / the ADR added in #631. Unblocks wvlet's `Native` workflow on `main`. **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 ac6b1dc commit a88aff6

7 files changed

Lines changed: 368 additions & 42 deletions

File tree

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
#!/usr/bin/env bash
2+
#
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:
5+
#
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).
10+
#
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.
14+
#
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.
18+
set -euo pipefail
19+
20+
shim="uni/.native/src/main/resources/scala-native/uni_curl_shim.c"
21+
obj="$(mktemp -d)/uni_curl_shim.o"
22+
23+
echo "== Compiling ${shim} with $(clang --version | head -1)"
24+
clang -c "${shim}" -o "${obj}" -Wall -Wextra -Werror
25+
26+
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"
33+
34+
if grep -qi 'curl_easy' "${obj}.undefined"; then
35+
echo
36+
echo "ERROR: the shim object references libcurl symbols. Downstream Scala Native projects that do"
37+
echo "not use CurlBindings never link -lcurl, so their build fails with 'undefined reference to"
38+
echo "curl_easy_setopt'. Resolve the symbols at runtime instead — see issue #622 and"
39+
echo "adr/2026-07-06-curl-shim-weak-linking.md."
40+
exit 1
41+
fi
42+
43+
echo
44+
echo "OK: compiles cleanly and references no libcurl symbol."

.github/workflows/test.yml

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,28 @@ jobs:
1414
pull-requests: read
1515
outputs:
1616
src: ${{ steps.filter.outputs.src }}
17+
native: ${{ steps.filter.outputs.native }}
1718
steps:
1819
- uses: actions/checkout@v7
1920
- uses: dorny/paths-filter@v4
2021
id: filter
2122
with:
2223
filters: |
24+
# Building Scala Native on Windows costs ~15 minutes, mostly toolchain setup, so the
25+
# Windows job sits behind this narrower filter: the C sources Scala Native compiles into
26+
# every downstream binary, the Native-only Scala, and the Scala Native version itself.
27+
native:
28+
- '.github/workflows/test.yml'
29+
- '**.c'
30+
- '**/.native/**'
31+
- 'project/**'
2332
src:
2433
- '.github/workflows/test.yml'
34+
- '.github/scripts/**'
2535
- 'project/**'
2636
- '**.scala'
2737
- '**.sbt'
38+
- '**.c'
2839
- '**.conf'
2940
- '**.json'
3041
- '**.js'
@@ -113,6 +124,11 @@ jobs:
113124
run: sudo apt-get update && sudo apt-get install -y libcurl4-openssl-dev
114125
- name: Scala Native test
115126
run: JVM_OPTS=-Xmx4g ./sbt "projectNative/test"
127+
# This build passes -lcurl unconditionally, so no Scala Native job here — on any OS — can
128+
# notice uni_curl_shim.c growing a reference to a libcurl symbol. Downstream projects that
129+
# never link libcurl can, and their build breaks (issue #622). Check the object directly.
130+
- name: Assert the curl shim references no libcurl symbol
131+
run: .github/scripts/check-curl-shim.sh
116132
- name: Publish Test Report
117133
uses: mikepenz/action-junit-report@v6
118134
if: always() # always run even if the previous step fails
@@ -121,6 +137,25 @@ jobs:
121137
check_name: Test Report Scala Native
122138
annotate_only: true
123139
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)
151+
needs: changes
152+
if: ${{ github.event_name != 'pull_request' || needs.changes.outputs.native == 'true' }}
153+
runs-on: windows-latest
154+
steps:
155+
- uses: actions/checkout@v7
156+
- name: Compile the shim and check for libcurl symbol references
157+
shell: bash
158+
run: .github/scripts/check-curl-shim.sh
124159
package_src:
125160
name: Verify packageSrc
126161
needs: changes

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

Lines changed: 110 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -33,24 +33,54 @@ either: Scala Native has no jar-level knob to conditionally include a resource
3333

3434
## Decision
3535

36-
Resolve the libcurl symbols lazily via
37-
`dlsym(RTLD_DEFAULT, "curl_easy_setopt")` — not via C-level `extern` — inside a
38-
`pthread_once` initialiser that stores each pointer in a `static` cache. The
39-
shim's `.o` then has zero references to libcurl symbols; downstream projects
40-
that don't use CurlBindings link cleanly. Projects that do use CurlBindings
41-
still pull libcurl in via `@link("curl")` on `CurlBindings.Extern`, and
42-
`RTLD_DEFAULT` finds the symbols inside the already-loaded libcurl at runtime.
36+
Resolve the libcurl symbols lazily, at first call, from whatever is already
37+
loaded into the process — not via C-level `extern` — inside a run-once
38+
initialiser that stores each pointer in a `static` cache. The shim's `.o` then
39+
has zero references to libcurl symbols; downstream projects that don't use
40+
CurlBindings link cleanly. Projects that do use CurlBindings still pull libcurl
41+
in via `@link("curl")` on `CurlBindings.Extern`, and the lookup finds the
42+
symbols inside the already-loaded libcurl at runtime.
43+
44+
The two primitives this needs — a lookup over all loaded modules, and a
45+
thread-safe run-once — are spelled differently per platform, so the file carries
46+
a `#if defined(_WIN32)` split:
47+
48+
| | POSIX | Windows |
49+
|---|---|---|
50+
| lookup | `dlsym(RTLD_DEFAULT, name)` | `GetProcAddress` over `EnumProcessModules` |
51+
| run-once | `pthread_once` | `InitOnceExecuteOnce` |
52+
53+
The Windows half exists because MSVC ships neither `<dlfcn.h>` nor
54+
`<pthread.h>`; the first version of this shim (v2026.1.17) included both
55+
unconditionally and broke every downstream Windows Scala Native build with
56+
`fatal error: 'dlfcn.h' file not found`.
4357

4458
The function-pointer typedefs are declared variadic
4559
(`typedef int (*fn)(void *, int, ...)`) so the call at the shim's own call site
4660
still emits the correct variadic calling convention — that's what fixes the
4761
original CURLE_URL_MALFORMAT bug from #580, and it works exactly the same via a
4862
variadic function-pointer as via a variadic extern.
4963

50-
If `dlsym` returns NULL (the shim is called from a build that somehow got the
64+
If the lookup returns NULL (the shim is called from a build that somehow got the
5165
wrappers linked in without libcurl present), the shim prints a pointer to #622
5266
on stderr and `abort()`s rather than jumping to NULL.
5367

68+
Two CI additions guard this, because the file's two failure modes need two
69+
different checks:
70+
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/`.
77+
- **`.github/scripts/check-curl-shim.sh`**, a step in the Linux Native job, which
78+
compiles the shim standalone and asserts via `nm -u` that the object names no
79+
`curl_easy_*` symbol. No Scala Native job can catch that regression on any OS:
80+
`build.sbt` passes `-lcurl` unconditionally, so uni's own binaries always
81+
resolve those symbols. Only a consumer that doesn't link libcurl breaks — the
82+
object inspection stands in for that consumer.
83+
5484
## Non-obvious points a future reader would otherwise reverse-engineer
5585

5686
### `weak_import` / `__attribute__((weak))` on the extern is *not* a substitute
@@ -83,41 +113,101 @@ separate — but Scala Native's own `nativelib` already links both, so the shim
83113
inherits them. No extra linker option is required from consumers. Verified by
84114
inspecting Scala Native's link line (`[pthread, dl, m, crypto, curl, z]`).
85115

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+
137+
### Windows: why module enumeration, and why `PSAPI_VERSION 2`
138+
139+
Windows has no `RTLD_DEFAULT`. `GetProcAddress` takes one `HMODULE` at a time,
140+
so the shim asks `EnumProcessModules` for every module loaded into the process
141+
and walks them in order — the process image comes first, matching `dlsym`'s
142+
search order. `GetModuleHandleA(NULL)` alone would only see the executable's own
143+
exports and would miss `libcurl.dll`.
144+
145+
`#define PSAPI_VERSION 2` before `<psapi.h>` redirects `EnumProcessModules` to
146+
`K32EnumProcessModules`, which lives in `kernel32.dll` and so is always linked.
147+
Under the default (version 1) the symbol resolves out of `psapi.lib`, and a
148+
jar-resource `.c` file cannot make downstream binaries pass an extra linker
149+
flag — the exact constraint that kills the weak-symbol approach on macOS.
150+
`_WIN32_WINNT` is floored at `0x0600` for `InitOnceExecuteOnce` (Vista+), for
151+
MinGW header sets that leave it unset.
152+
153+
`GetProcAddress` returns `FARPROC`; casting it straight to `void *` trips
154+
`-Wcast-function-type`, so the shim rounds it through `uintptr_t`.
155+
156+
### Windows: libcurl must be a DLL, not a static `.lib`
157+
158+
Runtime symbol lookup can only find *exported* symbols. A statically linked
159+
libcurl (vcpkg's `*-windows-static` triplets) exports nothing, so
160+
`GetProcAddress` returns NULL and the shim aborts with its diagnostic. Consumers
161+
must link libcurl dynamically — vcpkg's default `x64-windows` / `arm64-windows`
162+
triplets do exactly that, producing `libcurl.lib` as an import library for
163+
`libcurl.dll`.
164+
165+
This is not a Windows quirk so much as the price of the whole approach: a static
166+
libcurl on Linux (`libcurl.a`, no `--export-dynamic`) is invisible to
167+
`dlsym(RTLD_DEFAULT, ...)` in precisely the same way. The old `extern`-based shim
168+
worked with static libcurl on both — that capability is what was traded away to
169+
fix #622.
170+
86171
### `_GNU_SOURCE` is required on glibc
87172

88173
`RTLD_DEFAULT` is a GNU extension: glibc's `<dlfcn.h>` only defines it when
89174
`_GNU_SOURCE` is set. macOS libSystem exposes it unconditionally, so the macro
90175
is harmless there. The file defines `_GNU_SOURCE` at the top — dropping it
91176
would silently break Linux builds with `error: 'RTLD_DEFAULT' undeclared`.
92177

93-
### Lazy init needs `pthread_once`, not "same value written twice"
178+
### Lazy init needs a run-once guard, not "same value written twice"
94179

95180
An earlier draft argued no lock was needed because racing threads would race to
96181
write the same pointer value. That reasoning is wrong under C11: concurrent
97182
unsynchronised accesses to a non-atomic object where at least one is a write is
98183
a data race, i.e. undefined behaviour, regardless of the value written. The
99184
compiler is entitled to assume no such race exists and to re-order/eliminate
100-
the loads and stores accordingly. `pthread_once` gives a proper
101-
happens-before edge between the resolver's writes and every subsequent reader,
102-
at negligible cost after the first call (a plain "already done" flag check).
185+
the loads and stores accordingly. `pthread_once` (and `InitOnceExecuteOnce` on
186+
Windows) gives a proper happens-before edge between the resolver's writes and
187+
every subsequent reader, at negligible cost after the first call (a plain
188+
"already done" flag check).
103189

104190
### The variadic-typedef trick is the whole variadic story
105191

106192
The C ABI's variadic calling convention is determined by the *type at the call
107193
site*, not by whatever the symbol on the other end was compiled as. That is why
108194
calling through `uni_curl_setopt_fn` (declared variadic) reproduces #580's fix
109-
even though the pointer was obtained from `dlsym` (which has no type
110-
information). Do not "simplify" the typedef to a fixed-arity signature — that
111-
reintroduces the CURLE_URL_MALFORMAT bug from #580.
195+
even though the pointer was obtained from `dlsym` / `GetProcAddress` (neither of
196+
which carries type information). Do not "simplify" the typedef to a fixed-arity
197+
signature — that reintroduces the CURLE_URL_MALFORMAT bug from #580.
112198

113199
## Consequences
114200

115201
- Downstream Scala Native projects that don't use CurlBindings link cleanly
116202
again — #622 fixed.
117-
- Downstream projects that do use CurlBindings pay one `dlsym` per unique
118-
wrapper on first call (three lookups total across the process's lifetime);
119-
every subsequent call is a plain indirect function call.
203+
- Downstream projects that do use CurlBindings pay one symbol lookup per unique
204+
wrapper on first call (two lookups total across the process's lifetime); every
205+
subsequent call is a plain indirect function call. On Windows a lookup also
206+
walks the loaded-module list, which is still a once-per-process cost.
120207
- CurlBindings' `@link("curl")` remains load-bearing — it's how libcurl actually
121208
gets into the process. Do not remove it.
122-
- Removing the shim `.c` file, or any `dlfcn`/`stdio` include, would regress
123-
either the ABI fix or the link-error fix; keep both.
209+
- libcurl must be linked dynamically. Statically linked libcurl no longer works
210+
on any platform, Windows included; see above.
211+
- Removing the shim `.c` file, or any of its includes, would regress either the
212+
ABI fix, the link-error fix, or the Windows build; keep all three. The
213+
"Scala Native (Windows)" job and `check-curl-shim.sh` are what notice.

adr/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ Read the relevant ADR before modifying the area it covers. Add a new entry here
1212
- [`2026-06-30-sbt-uni-crossproject.md`](2026-06-30-sbt-uni-crossproject.md) — the `sbt-uni-crossproject/` build: a minimal, uni-owned sbt 2.x re-implementation of `portable-scala/sbt-crossproject` (which isn't ported to sbt 2.x), supporting only the `CrossType.Pure` layout uni uses. Read before touching that build; covers the single-plugin-for-all-three-platforms choice, the Scala 3 val-name macro replicating sbt's `KeyMacro.definingValName`, why internal materialization needs `new CrossProject(...)`, and the `given Conversion[Builder, CrossProject]` build trigger.
1313
- [`2026-06-30-sbt2-main-build-migration.md`](2026-06-30-sbt2-main-build-migration.md) — migrating the **main build** to sbt 2.x: swaps the unported third-party plugins for the uni-owned ones (`sbt-uni-crossproject`, `uni-jsenv-playwright`, `sbt-uni` for `sbt-revolver`). Read before touching `build.sbt` / `project/plugin.sbt`; covers the output-dir name collision (root → `uni-root`), `%%%``%%` and the `scalajs-test-interface_2.13` single-`%` exception, `Def.uncached` for `jsEnv`, and the `implicitConversions` import.
1414
- [`2026-07-06-plugin-extension-points.md`](2026-07-06-plugin-extension-points.md)`wvlet.uni.plugin` is built on typed `ExtensionPoint`s (identity-compared singletons; keyed points reject duplicate ids at activation), with `PluginContext` reduced to `contribute` + `onDeactivate`. Read before adding new contribution kinds: define a point next to the contributed type (`Command.point`, `RPCPlugin.routerPoint`) so dependency arrows point into `plugin`, never out of it.
15-
- [`2026-07-06-curl-shim-weak-linking.md`](2026-07-06-curl-shim-weak-linking.md)`uni_curl_shim.c` resolves libcurl's variadic setopt/getinfo via `dlsym(RTLD_DEFAULT, ...)` inside a `pthread_once` init, not C-level `extern`s, so downstream Scala Native builds that don't use `CurlBindings` still link (issue #622). Read before touching the shim: covers why `__attribute__((weak))` on the extern fails on macOS, why `RTLD_DEFAULT` (not `dlopen`) is the right lookup, why the variadic-typedef ABI trick from #580 is still load-bearing, why `_GNU_SOURCE` is required on glibc, and why lazy init needs `pthread_once` not just "same value written twice".
15+
- [`2026-07-06-curl-shim-weak-linking.md`](2026-07-06-curl-shim-weak-linking.md) — `uni_curl_shim.c` resolves libcurl's variadic setopt/getinfo from the process's already-loaded modules (`dlsym(RTLD_DEFAULT, ...)` on POSIX, `GetProcAddress` over `EnumProcessModules` on Windows) inside a run-once init, not C-level `extern`s, so downstream Scala Native builds that don't use `CurlBindings` still link (issue #622). Read before touching the shim: covers why `__attribute__((weak))` on the extern fails on macOS, why `RTLD_DEFAULT` (not `dlopen`) is the right lookup, why MSVC needs the whole `#if defined(_WIN32)` half (no `<dlfcn.h>`/`<pthread.h>`) and `PSAPI_VERSION 2`, why libcurl must now be linked dynamically, why the variadic-typedef ABI trick from #580 is still load-bearing, why `_GNU_SOURCE` is required on glibc, why lazy init needs a run-once guard not just "same value written twice", why a real Scala Native Windows CI job is impossible today (`NativeServer` needs POSIX `poll`), and why guarding this takes a standalone clang compile plus a separate `nm` check (the build's unconditional `-lcurl` blinds every native job to the #622 regression).

docs/guide/installation.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,13 @@ libraryDependencies += "org.wvlet.uni" %%% "uni" % "__UNI_VERSION__"
2626
libraryDependencies += "org.wvlet.uni" %%% "uni" % "__UNI_VERSION__"
2727
```
2828

29+
::: tip Scala Native and libcurl
30+
No system libraries are needed to build a Scala Native binary against uni. Only
31+
reaching for the HTTP client pulls in libcurl, which must then be present as a
32+
shared library — see
33+
[Linking libcurl on Scala Native](../http/client.md#linking-libcurl-on-scala-native).
34+
:::
35+
2936
## Imports
3037

3138
Common imports for getting started:

0 commit comments

Comments
 (0)