When a receiver flow is carried by more than one RTCP-capable peer,
send_nack_group historically sent every NACK to the eligible peer
with the lowest RTT. That is wrong when the lowest-RTT peer cannot
actually retransmit: e.g. a duplicate/relay feed that delivers the
same packets but keeps no retransmit buffer. NACKs go to it, the
peer that holds the buffer never hears them, and recovery stalls.
Add a per-peer recovery_priority (new ?recovery-priority=<n> URL
parameter, rist_peer_config.recovery_priority). NACK groups now go
to the eligible peer with the highest recovery_priority, ties broken
by lowest RTT. The default of 0 on every peer reduces exactly to the
prior lowest-RTT selection, so existing deployments are unchanged.
The selection predicate is factored into src/rist-nack-select.h
(rist_nack_peer_preferred) so it can be unit-tested without building
peers or starting threads.
Also name the weight=0 duplicate sentinel: new public macro
RIST_PEER_WEIGHT_DUPLICATE makes the load-balancer's "duplicate to
all peers" branch read as intent rather than a magic 0.
ABI: RIST_PEER_CONFIG_VERSION 4 -> 5 (one new trailing field,
defaulted to 0 by rist_peer_config_defaults_set; zero-initialised
configs keep legacy behaviour).
Tests:
- test/rist/unit/test_url_recovery_priority_parse.c: ?recovery-priority=
parsing through the public rist_parse_address2 API.
- test/rist/unit/test_nack_peer_select.c: priority/RTT selection,
tie-breaks, legacy-equivalence and unmeasured-RTT edge cases.
- meson test --suite unit: 8/8 OK.
- meson test (full): 51 OK, 11 Expected Fail, 0 unexpected.
Add a unit test asserting rist_peer_config_defaults_set_versioned() writes
only the fields defined at or below the requested version (split_mode and
merge_mode from version 1, profile and profile_set from version 4) and that
the rist_peer_config_defaults_set() inline records the caller's compiled
RIST_PEER_CONFIG_VERSION.
Document in NEWS the new versioned initialiser, the version guards in
parse_url_options() / store_peer_settings(), and the retained exported
rist_peer_config_defaults_set() symbol, which assumes version 0 for binaries
linked against an earlier library and therefore leaves version-1+ fields
untouched.
Introduces version checks and a versioned struct initialization mechanism for
`struct rist_peer_config` to protect against memory corruption and out-of-bounds
access when older client binaries run against a newer dynamic library.
Changes:
- Declared `rist_peer_config_defaults_set_versioned()` to initialize defaults
conditionally based on the version of the calling client.
- Redefined `rist_peer_config_defaults_set()` as a static inline function in
`peer.h` for external clients to capture their compiled version, while keeping
the non-inline legacy wrapper in `rist.c` (gated by `LIBRIST_INTERNAL` and
defaulted to version 0) for ABI linkage compatibility.
- Added version checks to `parse_url_options()` inside `src/rist-common.c` to prevent
writing out-of-bounds (`multicast_ttl`, `multicast_source`, `reflector`,
`local_port` require version >= 2; `srp_compat_legacy` requires version >= 3).
- Added version checks to `store_peer_settings()` inside `src/rist-common.c` to prevent
reading out-of-bounds when copying configuration settings to internal structures
(`reflector` requires version >= 2; `srp_compat_legacy` requires version >= 3).
Record the fix that clears the global logging settings when an
application frees its rist_logging_settings (directly or by destroying a
context that embeds them), preventing later logs from calling a freed
callback.
Fixes an access violation/use-after-free in client applications (such
as FFmpeg) when a stream is quickly closed and opened.
If a client allocates or embeds logging settings, they are copied into
the static global logging settings. When the stream is destroyed, the
memory hosting the logging callback and its user argument is freed.
If they are not unset from the global logger, subsequent library logs
routed through rist_log_priv3 (e.g., in background evsocket threads
or new context initialization) will attempt to use the dangling
callback argument, triggering a crash inside the logging callback.
This fixes the issue by unsetting the global logging configuration
if the settings structure being cleaned up matches the currently
configured global logging settings.
- Add rist_logging_unset_global_if_matches() to check and unset settings.
- Call it in rist_logging_settings_free2() when freeing settings directly.
- Call it in rist_receiver_destroy_local() and rist_sender_destroy_local()
to handle clients like FFmpeg that embed the settings structure.
Mirrors the existing peer_url label on rist_sender_peer_*. peer_id
is reissued (higher) on reconnect, so it isn't a stable per-peer
identifier; peer_url is.
Sender peers carry the URL through rist_prometheus_sender_add_peer.
Receiver peers are auto-discovered from the stats callback, so the
URL is added to the receiver-flow stats JSON ("url" field on each
peer object) and parsed at first registration in the exporter.
For listener-mode peers the URL lives on the parent (the inbound
child is created with url=NULL); the stats populator walks the
parent chain.
The unix-socket scraper exposed by ristreceiver / ristsender
(activated by --metrics-unixsock) returned empty or stub-only
bodies on a substantial fraction of scrapes in production.
Six separate defects in tools/prometheus-exporter.c contributed:
1. listen(fd, 1) -- a backlog of one is too small for any
real deployment where Prometheus + an external collector
may scrape within the same RTT. Replaced with SOMAXCONN.
2. accept() returning EINTR was treated as fatal and broke the
thread out of the loop, silently disabling the exporter for
the remainder of the process lifetime. EINTR is now retried.
3. The hot path issued a single unchecked write() with the
result discarded via (void)!. Partial writes and transient
EINTR therefore truncated the response, and an early peer
close raised SIGPIPE on the exporter thread. Replaced with
a write_all helper that loops on partial writes, retries
EINTR, and uses MSG_NOSIGNAL where available.
4. shutdown(SHUT_RDWR) immediately before close() can behave
as an abortive teardown on AF_UNIX SOCK_STREAM under some
kernels, discarding bytes that write() had just queued.
Switched to shutdown(SHUT_WR), which is the documented
"no more writes -- let the receive queue drain" sequence.
5. close() with unread bytes in the receive queue is the
biggest source of empty-body scrapes in the field. The
exporter is a dump-on-connect protocol that never reads
the client's request, but tools like nc / curl / the
Prometheus scraper itself all send "GET /metrics HTTP/1.0
\r\n\r\n". On Linux, close() on an AF_UNIX SOCK_STREAM
with unread input is treated as an abortive teardown: the
kernel sends RST and discards everything in the transmit
queue -- including the 10 KB body we just wrote. The
client sees ECONNRESET / EOF with zero bytes. We now
shutdown(SHUT_WR) and drain the receive queue with a
SO_RCVTIMEO-bounded recv loop before close, so the kernel
does a clean FIN-FIN handshake and the body actually
reaches the client. Confirmed by a Python probe that
went from 5/5 ECONNRESET to 5/5 success in the same
environment.
6. In single-stat-point mode (the default for ristreceiver
and ristsender) rist_prometheus_stats_format() was zeroing
container_count after every scrape. Any subsequent scrape
that arrived before the next 1 Hz stats callback then
emitted only HELP/TYPE/UNIT preambles with no data points,
which manifested in the field as metrics flickering and
downstream consumers parsing NaN / blank bandwidth. The
ring is now only reset in --metrics-multipoint mode;
single-point mode keeps the latest sample visible until
the next callback overwrites it. cleanup_stale_locked()
still ages stale flows / peers out after 15 s.
In single-stat-point mode the writer set container_offset = 1 after
each callback, but the format() macro reads only container[0..count-1]
and container_count stays at 1. container[0] was therefore frozen at
the value from the first callback while every subsequent callback
silently overwrote container[1], which the reader never touches.
Three sites had the same pattern:
- rist_prometheus_handle_client_stats (receiver flow stats)
- rist_prometheus_handle_client_stats (per-receiver-peer stats)
- rist_prometheus_handle_sender_peer_stats
NEWS entry added under 0.2.18 Bug Fixes.
The original librist convention for URL parameters that select from a
small fixed enum is numeric values (aes-type, congestion-control,
timing-mode, reflector, and as of the previous commit profile).
?srp-compat= was added in v0.2.18-rc1 with a symbolic alias ("legacy")
and a permissive parser that mapped any unrecognised value, including
a typo of the alias, to 0 (compliant). Tightened to numeric-only
before v0.2.18 final.
The parser now accepts only 0 or 1. Out-of-range, non-numeric, and
trailing-garbage values cause rist_parse_address2 to return non-zero
with stderr output.
Tests
-----
- test/rist/unit/test_url_srp_compat_parse.c (new): two accepted forms
plus six rejected forms.
- meson test --suite unit: 6/6 OK (was 5/5).
- meson test --suite url-profile: 4/4 OK.
- meson test (full): 49 OK, 11 Expected Fail, 0 unexpected.
RIST_URL_PARAM_PROFILE has been declared in the public urlparam.h header
since at least the rist2rist merge but the URL parser never honored it
and the field had no library-side effect. This commit makes the
parameter do what the public header has always advertised: select the
wire profile of the context that owns the peer, from inside the URL
itself.
The override is implemented entirely in the library so every consumer
that already passes URLs through rist_parse_address2 + rist_peer_create
picks it up without application-side changes.
URL syntax
----------
?profile=0 Simple
?profile=1 Main
?profile=2 Advanced
Out-of-range, non-numeric, or trailing-garbage values cause
rist_parse_address2 to return non-zero with stderr output.
ABI
---
RIST_PEER_CONFIG_VERSION bumps from 3 to 4. Two trailing fields added
to struct rist_peer_config (profile, profile_set).
Tests
-----
- test/rist/unit/test_url_profile_parse.c: parser assertions including
the rejected forms.
- test/rist/unit/test_url_profile_override.c: three scenarios through
the public API - override applies, conflicting peer refused, late
peer refused.
- test/rist/meson.build: four end-to-end tests where rist_*_create is
called with a profile that differs from the URL ?profile= value;
both ends must converge on the URL profile for the test to pass.
- meson test --suite unit: 5/5 OK
- meson test --suite url-profile: 4/4 OK
- meson test (full): 48 OK, 11 Expected Fail, 0 unexpected.
NEWS updated under ABI/API and Bug Fixes.
The receiver-side flow bitrate fields (`bitrate`, `bitrate_retries`,
`bitrate_rejected`, `bitrate_ts_nulls`) are only updated when a packet
arrives on the corresponding path, because the bw->bitrate field is
recomputed inside rist_calculate_flow_bitrate() and that helper is only
called from the ingest path. As soon as the traffic on a counter stops
(e.g. retries cease), the matching `bitrate_*` field stays frozen at
the last value it was assigned while traffic was flowing.
Add a small rist_refresh_flow_bitrate() helper that recomputes
bw->bitrate with zero new bytes, mirroring what
rist_calculate_bitrate(0, ...) already does for the sender peer stats
path. The helper deliberately skips the per-flow inter-packet-spacing
bookkeeping that lives in rist_calculate_flow_bitrate(), so calling it
from the stats tick does not perturb the IPS counters.
Call the new helper for all four flow bitrate counters at the start of
the JSON/binary stats emission in rist_receiver_flow_statistics(), so
each counter naturally decays to zero when its traffic stops.
PROMETHEUS_COUNTER_PRINT_R wrote the per-sample timestamp using
"%"PRIu64"\n" with no leading separator, so when single_stat_point
is false (the multiple_metric_datapoints=true setup), every counter
row came out as
rist_client_flow_received_packets_total{...} 62001780601685
i.e. the value and the timestamp glued together. The sibling
PROMETHEUS_GAUGE_PRINT_R macro uses " %"PRIu64"\n" and emits the
correct "<value> <timestamp>" form. OpenMetrics requires the
timestamp to be space-separated from the value, so a scraper
reading these lines either rejects them or stores one large bogus
number.
Add the missing space. Only the multiple-datapoint branch is
affected; the single_stat_point branch omits the timestamp
entirely and is unchanged.
The Prometheus exporter publishes flow-level aggregates for receivers
(rist_client_flow_*) and per-peer breakdowns for senders
(rist_sender_peer_*) but no per-peer view on the receive side, even
though that data is already populated in rist_stats_receiver_flow.peers[].
Add the receiver-side counterpart as rist_receiver_peer_* with labels
{peer_id, flow_id, receiver_id}, mirroring the sender_peer schema:
rist_receiver_peer_bandwidth_bps gauge
rist_receiver_peer_avg_bandwidth_bps gauge
rist_receiver_peer_received_data_packets counter
rist_receiver_peer_received_bytes counter
rist_receiver_peer_received_rtcp_packets counter
rist_receiver_peer_sent_rtcp_packets counter
rist_receiver_peer_rtt_seconds gauge
rist_receiver_peer_avg_rtt_seconds gauge
Entries auto-register on first sight of a (receiver_id, flow_id,
peer_id) tuple, so callers do not need to wire peer_ids in at peer
creation time. Re-uses the existing stale-entry sweep (15s) and the
same realloc/abort-on-OOM idiom as sender_peers.
No public API change.
Public header <librist/transport.h> (0.2.18-rc1) typedefs ssize_t
on Windows under an _SSIZE_T_DEFINED guard. common/attributes.h
defines the same type without a guard, so any TU that includes
both pulls in a duplicate definition and trips MSVC C4142
(benign redefinition). Wrap each of the three Windows code paths
here in the same _SSIZE_T_DEFINED guard so the two headers are
order-independent. Closes the warning reported in #216.
The main librist library target already picks up the compat/msvc
<stdatomic.h> shim when MSVC's system header requires
/experimental:c11atomics, but the srp_unit test target compiles
random.c (and friends) without that dependency and so hits
'C atomic support is not enabled' on plain MSVC builds. Mirror
the library target's deps so the test follows the same code path.
Closes the second build error in #216.
MSVC has no <sched.h> and pulls calloc's prototype in via <stdlib.h>
(transitively, through <cmocka.h>), so the musl-specific shim is
only needed on POSIX targets. Closes the first build error in #216.
Bump project version to 0.2.18, API to 4.10.0, ABI to 13:0:9
(soversion 4 unchanged, binary-compatible with 0.2.15/0.2.16/0.2.17).
Headlining the release:
- srp-compat=legacy URL opt-in restores pre-0.2.16 interop with the
RFC 5054 PAD-compliant exchange landed in 0.2.16.
- Windows XP support restored: rist_transport_poll() uses select()
via the existing poll_win.c shim instead of WSAPoll() (Vista+).
- rist_peer_recv() accepts both numeric EAGAIN values (11 and 35) so
custom transport_ops backends in cross-compile environments work.
- <librist/transport.h> now defines ssize_t on Windows (#214).
- CI: srp_unit_test now runs via a cmocka wrap subproject, closing
the silent-skip that hid the SRP fixture drift reported in #215.
- Replace the placeholder "No source or binary changes" ABI/API entry
with the actual change: RIST_PEER_CONFIG_VERSION 2 -> 3 plus the
one new field (srp_compat_legacy). Existing zero-initialised
peer configs continue to work; the new field defaults to 0.
- Promote the SRP interop note to its own section and document the
new srp-compat=legacy URL option together with the advisory hint
emitted on M1/M2 failure.
- Mention the four new end-to-end SRP unit tests under CI / Test
Coverage alongside the existing fixture-regeneration entry.
Adds srp_run_exchange() — a self-contained authenticator+client
handshake on the 2048-bit NG_DEFAULT group using only the public SRP
API, then drives it through four scenarios:
1. Both ends in PAD mode (default) -> verify_m1 == 0
2. Both ends in legacy unpadded mode -> verify_m1 == 0
3. Authenticator PAD, client legacy -> verify_m1 != 0
4. Authenticator legacy, client PAD -> verify_m1 != 0
Cases 3 and 4 are the "operator forgot to set srp-compat on one side"
configurations. They MUST fail at M1 — otherwise the bypass would be
silently leaking through. The tests deliberately do NOT assert on the
specific cross-mode failure mode, because the SRP-6a identity
(A * v^u)^b = (B - k*g^x)^(a + u*x)
only holds when both peers use the same k, so no purely-local check
can produce a deterministic cross-mode M1 to compare against.
Also propagates the new legacy_pad parameter through every existing
ctx_create call site so the deterministic fixture tests continue to
exercise the spec-compliant path.
0.2.16 made TLS-SRP RFC 5054 / TR-06-2 §6.4 compliant by adding the
PAD operation to the u and k hash inputs. That change breaks
SRP-authenticated handshakes against peers still running 0.2.15 or
earlier. The recommended fix is to upgrade both ends, but
mixed-version deployments need a temporary escape hatch.
This change adds an opt-in legacy wire-format mode:
rist://user:pass@host:port?srp-compat=legacy
Must be set on BOTH ends. The library logs an explicit RIST_LOG_WARN
banner on startup whenever the legacy mode is active, marking it as
not TR-06-2 / RFC 5054 compliant. Default behaviour is unchanged.
Failure-side diagnostic
-----------------------
On SRP M1 (authenticator) or M2 (client) verification failure, the
library now emits a one-shot INFO hint pointing at this option as a
possible cause. The hint is advisory only — the SRP-6a transcript
binds the M1/M2 hashes to whichever wire mode the authenticator used
when constructing B, so there is no purely-local way to distinguish
"wrong password" from "peer is on the other wire format" once the
handshake has reached M1. Both possible causes are surfaced.
Plumbing
--------
- include/librist/peer.h: bump RIST_PEER_CONFIG_VERSION 2 -> 3, add
one trailing int (srp_compat_legacy); zero-initialised configs and
older clients keep the spec-compliant default.
- include/librist/urlparam.h: new RIST_URL_PARAM_SRP_COMPAT.
- src/rist-common.c: parse srp-compat=legacy | 1 into the new field
(anything else, including the explicit "auto", leaves PAD mode on).
store_peer_settings() also copies the flag into peer->config so
rist_enable_eap_srp_2(), which runs after the caller's config may
have been freed, can still see it.
- src/crypto/srp.{c,h}: new librist_crypto_srp_hash_2_bignum_unpadded()
helper (the pre-0.2.16 form) and a single srp_hash_uk() dispatch.
Both authenticator and client SRP contexts grow a bool legacy_pad
field set at ctx_create time; the four u/k hash sites pick padded
vs unpadded based on it.
- src/proto/eap.c: eapsrp_ctx grows srp_legacy_pad + a one-shot
srp_legacy_peer_warned latch; the flag is read from
peer->config.srp_compat_legacy when enable_eap_srp_2 builds the
ctx (both authenticator and authenticatee paths), propagated by
eap_clone_ctx, and passed into both SRP ctx_create calls.
Default behaviour is unchanged: peers without srp-compat=legacy keep
the PAD-compliant exchange.
Three additions under 0.2.18 in development:
- SRP interop note explaining why 0.2.16+ does not handshake
with 0.2.15 (PAD compliance is the reason; symmetric upgrade
required).
- Bug-fix entry for the new Windows ssize_t typedef (#214).
- CI/test coverage entry noting libcmocka-dev install and the
PAD-compliant test fixture regeneration (#215).
The recvfrom and sendto callbacks in struct rist_transport_ops
return ssize_t. On Windows that type is not a Win32 standard
type and is not provided by <winsock2.h>, so a downstream Windows
project that #includes <librist/transport.h> fails to compile
with "unknown type name 'ssize_t'".
Library-internal code worked because librist's private
common/attributes.h provides a typedef, but that header is not
installed. Add a public typedef inside the existing _WIN32 block,
guarded by _SSIZE_T_DEFINED to coexist with MSVC's CRT and other
public Windows headers that already conditionally define it.
Fixes#214.
Since 0.2.16 the SRP authenticator zero-pads the u and k hash inputs
to N-length per RFC 5054 §2.6 (and therefore TR-06-2). The fixtures
in srp_examples.c were captured against the pre-PAD code and have
failed deterministically since 0.2.16 with both gnutls, system
mbedtls 3.6, and bundled mbedtls — see #215.
A separate hardening change also enforced a 1024-bit minimum for
caller-supplied N in librist_crypto_srp_client_ctx_create(), which
caused test_srp_client_ctx_create (and three downstream tests) to
deref a NULL ctx and segfault on the deprecated 512-bit modulus.
Switch the deterministic setup to the 2048-bit RFC 5054 group
(NG_DEFAULT) and re-record every fixture from the current
PAD-compliant exchange. The DEBUG_USE_EXAMPLE_CONSTANTS hooks in
srp.c still pin a, b, and salt, so the run remains deterministic.
test_srp_client_ctx_create now exercises both the default_ng=true
path and a custom-N path with NG_2048, asserts non-NULL on every
create, and uses the standard sizeof()/mbedtls_mpi_size() byte
widths instead of the obsolete 64-byte buffers.
Fixes#215 (segfaults + hash mismatches).
srp_unit_test had been silently skipping in test-ubuntu because the
runner image (libplacebo-ubuntu-jammy) ships without libcmocka-dev
and does not give the CI user write access to /var/lib/apt/lists, so
an in-script apt-get install cannot fix it. The silent skip is how
the SRP fixture drift reported in #215 reached a release tag.
Switch the meson detection from cc.find_library('cmocka', required:
false) to dependency('cmocka', required: false, fallback:
['cmocka', 'cmocka_dep']) and add subprojects/cmocka.wrap pointing
at WrapDB cmocka_1.1.8-1. When the runner image has cmocka the
system copy is used; otherwise meson builds it as a subproject.
Existing .gitignore rules (/subprojects/* with !*.wrap) keep the
fetched source out of the tree.
srp_unit's link line now picks up bcrypt on Windows because
random.c calls BCryptGenRandom; the main librist target was already
pulling it in via the top-level deps list, but srp_unit links
random.c directly.
Drop the broken apt-get line from the test-ubuntu script and add
retry: 2 to both test jobs to absorb the simple+multicast environmental
flake that surfaces on the shared runner regardless of branch.
The shared CI runner used by test-win64 cannot complete the
send/receive tests within meson's default 30s per-test timeout when
the binaries are executed under wine. Recent pipelines on master
have been showing 38-41 of 55 tests marked TIMEOUT (with 0-3 real
failures) while the same suite passes on test-ubuntu.
The logs show peer authentication completing, packets flowing, NACK
/ retransmit loops operating normally — there are no crashes,
asserts or hangs, only tests being killed at the 30s boundary
because the wine layer adds enough overhead per test that they
cannot reach their target packet count in time.
Mark test-win64 as allow_failure so wine throughput jitter does not
block unrelated MRs. The job still runs and its log is still
uploaded on failure, so genuine Windows-runtime regressions remain
visible — they just no longer flip the overall pipeline status.
A TODO is left in the file describing the two ways to drop
allow_failure: bump meson's per-test timeout multiplier for this
job, or move it to a faster runner.
By default ristsender couples the input flow to RIST peer state:
multicast input groups are bound but the IGMP join is deferred until
at least one RIST output peer has completed its handshake, and
incoming UDP data is dropped until peer_connected_count > 0. That
keeps a freshly-started sender from generating multicast traffic the
network has not yet asked for and from buffering data nobody will
ever read, which is the right behaviour for point-to-point RIST.
The peer-handshake gate is a poor fit for broadcast topologies where
the sender is meant to stream regardless of who is (or isn't)
listening — multicast output in simple/main/advanced profile, or
ristsender as a fan-out in front of non-RIST consumers.
Add --blind-send. When set:
- the multicast input socket performs udpsocket_join_mcast_group()
immediately instead of marking it mcast_deferred, and
- input_udp_recv() writes data to the RIST context unconditionally.
Default behaviour is unchanged when the option is absent.
When a custom transport_ops backend returns -1 from recvfrom() to signal
"no data ready", librist's poll loop relies on errno == EAGAIN to treat
the call as transient and silently retry. In most environments this is
straightforward, but cross-compilation setups can end up with the
EAGAIN macro resolving to a different numeric value than the one the
custom backend actually wrote into errno.
This has been observed concretely with VLC's WebAssembly build, where
librist is compiled by Emscripten as part of VLC's contrib system: the
WebTransport-backed transport_ops returns -1 with errno = EAGAIN (= 11
in musl/Emscripten), but librist's own translation unit ends up with
EAGAIN expanding to 35 due to header search paths from the host
toolchain leaking into the cross build. The result was the poll loop
mistaking the transient case for a hard error and spamming
"Receive failed: errno=11" until the RIST handshake gave up.
Recognise both canonical EAGAIN errno values (11 on Linux / Emscripten /
musl, 35 on macOS / *BSD / iOS) explicitly when ret == -1. On any
platform where EAGAIN resolves consistently the existing macro check
already returns first, so this addition is unreachable and behaviour is
unchanged. It only fires in the pathological cross-compile case where
the macro and the runtime errno disagree, and protects the same set of
errno values that POSIX recvfrom(2) is documented to use for the
non-blocking "no data" path.
The Windows path (_WIN32) is unaffected.
librist 0.2.17 introduced a pluggable transport abstraction
(rist_transport_set + companion rist_transport_poll wrapper) that on
Windows fell back to WSAPoll(). WSAPoll is a Vista+ symbol, so adding
it to the librist link surface dropped runtime support for Windows XP
on the rist_transport_poll() code path — a regression compared with
0.2.16, which only ever called select() on Windows (via the
public-domain poll() shim included from contrib/poll_win.c into
src/libevsocket.c since 2019).
Route rist_transport_poll() through that same select()-based shim
instead of WSAPoll() so the pluggable-transport layer preserves XP
compatibility. The fix mirrors libevsocket.c's pattern: include
contrib/poll_win.c as a translation-unit-private static function on
Windows, then call poll() unconditionally on both platforms.
Also expose <winsock2.h>'s struct pollfd declaration at the prototype
site (transport-private.h) so the prototype and definition agree even
when callers pin _WIN32_WINNT below 0x0600 (e.g. VLC's 3.0.x contrib
system, which targets legacy Windows XP). The redefinition is
header-only and pulls in zero Vista+ runtime symbols — only the
struct pollfd typedef visibility changes.
After the fix, mingw-w64 cross-built librist.dll has WSAPoll removed
from its WS2_32.dll import table (verified locally with `objdump -p`);
XP runtime support is preserved.
Reported by Steve Lhomme (@robUx4) during review of VLC merge request
videolan/vlc!9297.