Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
cares-rs
Safe, threadless, completion-driven Rust 2024 bindings for upstream c-ares, built on liburing-rs. c-ares is compiled with CARES_THREADS=OFF; there is no executor runtime, background event thread, or readiness reactor.
Why cares-rs?
Name resolution sits before the first connection, but it can also interrupt an
otherwise healthy client when a pool reconnects or an endpoint changes. A
hostname lookup through getaddrinfo or std::net::ToSocketAddrs can block the
calling thread. On a worker responsible for many live connections, that pause
holds up unrelated traffic as well as the connection being opened.
An asynchronous resolver avoids that blocking call, but changes how the client must run. For example, Hickory's Tokio integration drives lookups as futures on a runtime. Adopting that integration in a synchronous client means propagating asynchronous signatures or bridging into an executor; its task scheduling also becomes part of the connection's latency budget. Thread-enabled c-ares builds offer another integration model, with channel locking and optional event-thread processing. A worker that already owns its I/O loop needs DNS progress to fit that loop instead.
cares-rs builds c-ares with CARES_THREADS=OFF and keeps each resolver on one
owner thread. In shared-ring applications, ProactorResolver submits socket
interests and retry timers to the caller's existing liburing::Proactor.
Answers, UDP-to-TCP fallback, timeouts, and follow-up queries advance through the
same completion dispatcher as the client's other I/O. There is no resolver
thread to synchronize with, and the caller keeps control of the timeout budget
and of how long shutdown may drain pending work.
That makes a failed lookup easier to inspect. A breakpoint can follow query
submission, socket or timer completion, and the callback on the same thread.
Borrowed DNS records are scoped to that callback, while ChannelCtx makes the
allowed follow-up operations explicit. Engineers and AI coding tools can reason
about these visible lifetimes without carrying resolver state across .await
boundaries or guessing when a background task will release it.
Design
- Threadless c-ares. The native library is built with
CARES_THREADS=OFFandCARES_SHARED=OFF; it contains no pthread mutexes, no event thread, and noARES_OPT_EVENT_THREAD. - Single-thread resolver.
Resolveris!Send + !Sync. Socket readiness, retry deadlines, and user callbacks all run on the thread that owns it. - Two completion drivers. Standalone mode uses
wait_completions,poll_completions, orresolve. Shared-ring mode keepsProactorResolverin caller-owned state and passes a staticCompletionDispatcherto the pure Rustliburing::Proactor. DNS-specific POLL_ADD readiness handling stays inside cares; the ring holds no resolver or handler registry. - Safe callback boundary.
DnsRecordRef<'cb>cannot escape its callback.ChannelCtx<'cb>exposes only re-entrant-safe operations, and panics are caught at the FFI boundary and poison the resolver instead of unwinding into C.
Library initialization uses atomic first/last-reference transitions. Resolver handles remain confined to their owner thread; socket progress does not wait on global initialization state.
All callbacks run synchronously on the thread that drives the resolver. A callback may submit follow-up queries through ChannelCtx; it cannot recursively drive the resolver.
The workspace targets Linux x86_64 and pins a nightly toolchain in rust-toolchain.toml.
Crates
| Crate | Purpose |
|---|---|
cares-sys |
FFI bindings and the static c-ares build. |
cares-macros |
DnsRecordExtract and cares_query_table! proc macros. |
cares |
Safe resolver, record models, and the proactor driver. |
The crates are path dependencies in one Cargo workspace; crates/cares/tests/ holds the integration suites.
Quick Start
use Duration;
use ;
Callbacks receive a ChannelCtx and a borrowed-or-owned record; use submit_query for completion-driven work.
Examples
Three self-contained examples under crates/cares/examples/ need no external network:
dns_packet_codec_and_ednsbuilds and parses wire messages with an EDNS0OPTrecord and typed answers.standalone_resolver_and_recordsruns local UDP/TCP queries throughwait_completions, including a re-entrant CNAME lookup.proactor_shared_ring_resolverdrivesProactorResolveron a realliburing::Proactor; a truncated UDP answer forces TCP fallback and socket-bridge growth. It requires a kernel with io_uring and fails if the ring cannot be created.
The codec example is pure in-memory; the two resolver examples bind a deterministic 127.0.0.1:0 responder. Each prints a short summary and exits with status 0.
Shared-Ring Shutdown
Call driver.flush(&mut ring) after submitting queries, then drive the shared
ring with ring.wait_completions(min, timeout, &mut dispatcher). Route the DNS
subsystem to driver.dispatch(completion, submissions); the driver remains
owned by the caller throughout. Completion routing uses liburing OperationId
and OperationPayload; the completed identity is Completion::operation_id.
driver.begin_shutdown(&mut ring) rejects new queries and cancels existing
queries and registrations. Continue flush and caller-owned completion dispatch
within your remaining deadline until driver.has_pending_io() is false.
inflight_count() includes polls, timers, and explicit cancel acknowledgments;
a cancel ACK does not retire its target. No poll or timer is rearmed during
shutdown. If the deadline expires, retain the entire (ring, driver) owner for
later draining. Cancellation controls also need operation/staging credit; peer
completions can release it when the shared ring is full.
driver.shutdown_within(&mut ring, budget) and driver.shutdown(&mut ring)
(the latter has a five-second budget) drive a DNS-only dispatcher and leave the
ring open. Shared-ring applications use their worker dispatcher instead. Close
the ring only after every subsystem has retired its I/O. Dropping a driver early
retains pending timeout operands because their final CQEs have not proven reuse.
Build and Test
Run every command from the workspace root. GitHub-hosted CI runs the Cargo checks directly.
RUSTDOCFLAGS="-D warnings"