Expand description
hick
Batteries-included, runtime-agnostic mDNS / DNS-SD for Rust — a Sans-I/O protocol core with pluggable async drivers.
§Introduction
hick is the high-level entry point to the hick mDNS family. It wires the
Sans-I/O protocol core (mdns-proto) to a ready-to-use async driver
(hick-reactor) so you can publish and discover services on the local
network in a few lines, with tokio out of the box.
mDNS (Multicast DNS, RFC 6762) and DNS-SD (RFC 6763) let peers discover one another on a local link with no central DNS server — the basis of “zeroconf” service discovery. Note that many cloud and shared-infrastructure networks restrict multicast, so mDNS is best suited to office, home, or private networks.
§Highlights
- Sans-I/O core. All protocol logic lives in
mdns-protoas pure state machines — no sockets, threads, or clocks — making it deterministic and exhaustively unit-tested. The drivers only shuttle bytes and time in and out. - Runtime-agnostic. Drive it from
tokio,smol, orcompio(thread-per-core) with no change to protocol behavior. no_stdand bare-metal. The core runs onalloc— orheapless, with no allocator at all — andhick-smoltcp/hick-embassybring full mDNS to embedded targets over smoltcp.- RFC 6762 / 6763 conformant. Probing and announcing, name-conflict detection with automatic renaming, known-answer and duplicate-question suppression, TTL-accurate caching, and TTL=0 goodbyes on withdrawal.
unsafe-minimal. The protocol core is#![forbid(unsafe_code)];unsafeis confined to the platform socket and control-message plumbing.- Observable, à la carte. Opt into
tracingspans,metricscounters, pollablestats, ordefmton embedded — each behind a feature flag and compiled out when unused.
§The family
The crates split protocol logic from I/O, mirroring the quinn layering:
| Crate | Role |
|---|---|
hick | this crate — batteries-included facade (core + default driver) |
mdns-proto | Sans-I/O protocol state machines (no_std-capable) |
hick-udp | multicast UDP socket setup |
hick-reactor | runtime-agnostic async driver (tokio & smol) |
hick-compio | compio (thread-per-core) async driver |
hick-smoltcp | bare-metal mDNS engine over smoltcp (no_std + alloc) |
hick-embassy | embassy async driver, built on hick-smoltcp (no_std + alloc) |
§Installation
[dependencies]
hick = "0.1" # tokio runtime by defaultFor smol instead of tokio:
[dependencies]
hick = { version = "0.1", default-features = false, features = ["smol"] }For the compio (completion-based, thread-per-core) runtime:
[dependencies]
hick = { version = "0.1", default-features = false, features = ["compio"] }For bare-metal (no_std) targets, enable smoltcp (the runtime-agnostic
engine) or embassy (the embassy-net async driver) — neither pulls in std:
[dependencies]
hick = { version = "0.1", default-features = false, features = ["embassy"] }On cores without native atomic CAS (Cortex-M0+ / thumbv6m / RP2040), use the
no-atomic tier instead — smoltcp-no-atomic or embassy-no-atomic — which
swaps the refcounted Name / rdata onto portable-atomic (clone via a
critical-section impl your binary provides):
[dependencies]
hick = { version = "0.1", default-features = false, features = ["embassy-no-atomic"] }Each driver is also published as a standalone crate (hick-compio,
hick-smoltcp, hick-embassy) if you would rather depend on it directly.
The minimum supported Rust version (MSRV) is 1.91.0 (edition 2024).
§Example
Common, runtime-agnostic types (Name, QuerySpec, ServiceSpec, …) live at the
crate root; the runtime-pinned entry point and driver handles live in the per-runtime
module (hick::tokio, hick::smol).
use hick::{Name, QuerySpec, ServiceRecords, ServiceSpec, wire::ResourceType};
use hick::tokio::{server, QueryEvent, ServerOptions};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Start the driver, pinned to the tokio runtime.
let endpoint = server(ServerOptions::default()).await?;
// Advertise a service. Keep the returned handle alive to keep advertising;
// dropping it withdraws the service (TTL=0 goodbye) and unregisters it.
let mut records = ServiceRecords::new(
Name::try_from_str("_http._tcp.local.")?, // service type
Name::try_from_str("My Device._http._tcp.local.")?, // instance
Name::try_from_str("my-device.local.")?, // host
80, // port
120, // TTL (seconds)
);
records.add_a([192, 168, 1, 10].into());
let _service = endpoint.register_service(ServiceSpec::new(records)).await?;
// Discover services of a type on the local link.
let mut query = endpoint
.start_query(QuerySpec::new(
Name::try_from_str("_ipp._tcp.local.")?,
ResourceType::Ptr,
))
.await?;
while let Some(event) = query.next().await {
match event {
QueryEvent::Answer(answer) => println!("{answer:?}"),
QueryEvent::Terminal(_) => break,
}
}
Ok(())
}§Feature flags
tokio(default) — drive thetokioruntime viahick-reactor.smol— drive thesmolruntime.compio— drive thecompio(thread-per-core) runtime viahick-compio.smoltcp— expose the bare-metalhick-smoltcpengine (no_std+alloc).embassy— expose thehick-embassyasync driver (no_std+alloc).smoltcp-no-atomic/embassy-no-atomic— the same engine / driver on theno-atomictier, for cores without native atomic CAS (Cortex-M0+ / thumbv6m / RP2040). Refcounted Name / rdata useportable-atomic(clone via acritical-sectionimpl your binary provides). Pick exactly one tier per engine.tracing— forward structuredtracingspans/events from every enabled driver and the proto core.stats— enableno_std-safe atomic counters; read a snapshot viaendpoint.stats().metrics— bridge counters to themetricsfacade (Prometheus/StatsD exporters). Impliesstats.defmt— forwarddefmtlog events to the bare-metal drivers (hick-smoltcp/hick-embassy). Has no effect when only std drivers are enabled.
§Observability
§tracing — structured events and spans
Add features = ["tracing"] and install a subscriber in main:
fn main() {
tracing_subscriber::fmt().init();
// … start your endpoint …
}The driver task and mdns-proto emit tracing events at DEBUG / INFO /
WARN level during probing, service registration, conflict resolution, cache
updates, and query completion.
§metrics — Prometheus / StatsD counters
Add features = ["metrics"] and install a recorder before starting the
endpoint:
// Example using the prometheus exporter from the `metrics-exporter-prometheus` crate:
metrics_exporter_prometheus::PrometheusBuilder::new().install().unwrap();All mDNS counters (packets, bytes, cache hits, queries, …) are forwarded
automatically under mdns_* metric names.
§stats — polling counters without a recorder
When you only need periodic read-outs without a full metrics exporter,
enable stats and call endpoint.stats():
use hick::tokio::{server, ServerOptions};
let endpoint = server(ServerOptions::default()).await?;
// … run for a while …
let snap = endpoint.stats();
println!("rx={} tx={} cache_size={}", snap.packets_rx, snap.packets_tx, snap.cache_size);stats() returns a hick_trace::stats::StatsSnapshot — a plain Copy
struct of u64 fields covering packets, bytes, cache, queries, services,
probes, conflicts, and more. See the hick-trace crate for the full list.
§defmt — bare-metal embedded logging
For no_std targets using hick-smoltcp or hick-embassy, enable defmt
instead of (or alongside) stats. Log events are forwarded through the
defmt framework. On std drivers this feature is a no-op.
Per-crate observability details: hick-trace (shim) · mdns-proto ·
hick-reactor · hick-compio · hick-smoltcp · hick-embassy ·
hick-udp.
§License
hick is under the terms of both the MIT license and the Apache License
(Version 2.0).
See LICENSE-APACHE, LICENSE-MIT for details.
Copyright (c) 2025 Al Liu.
Re-exports§
pub use mdns_proto as proto;pub use hick_compio as compio;compiopub use hick_smoltcp as smoltcp;smoltcp-no-atomicorsmoltcppub use hick_embassy as embassy;embassy-no-atomicorembassy
Modules§
- smol
smol smol-pinned mDNS driver — same shape astokio.- tokio
tokio tokio-pinned mDNS driver: the endpoint constructor (server) plus the driver handle types (themselves runtime-agnostic).- wire
- The wire codec (
mdns_proto::wire) — parser and encoder. Available in every build tier (no storage tier required), so it is always re-exported. mDNS wire format — panic-free parser and encoder. mDNS wire format — panic-free, no_alloc-capable parser and encoder.
Structs§
- Collected
Answer compioorembassy-no-atomicorembassyorsmolorsmoltcp-no-atomicorsmoltcportokio - Common, runtime-agnostic protocol types — re-exported from
mdns_protofor ergonomics. Service / query specs, records, names, and update events are identical for every driver, so they live at the crate root; the runtime-specific entry points (the endpoint constructor and its handle types) live in the per-runtime driver modules (tokio,smol) or driver crates (compio,smoltcp,embassy). These require a proto storage tier, which every runtime / driver feature enables, so the re-export is gated on having one. One collected answer record for a Query. - Name
compioorembassy-no-atomicorembassyorsmolorsmoltcp-no-atomicorsmoltcportokio - Common, runtime-agnostic protocol types — re-exported from
mdns_protofor ergonomics. Service / query specs, records, names, and update events are identical for every driver, so they live at the crate root; the runtime-specific entry points (the endpoint constructor and its handle types) live in the per-runtime driver modules (tokio,smol) or driver crates (compio,smoltcp,embassy). These require a proto storage tier, which every runtime / driver feature enables, so the re-export is gated on having one. Owned, canonical DNS name (lowercased on construction). - Query
Spec compioorembassy-no-atomicorembassyorsmolorsmoltcp-no-atomicorsmoltcportokio - Common, runtime-agnostic protocol types — re-exported from
mdns_protofor ergonomics. Service / query specs, records, names, and update events are identical for every driver, so they live at the crate root; the runtime-specific entry points (the endpoint constructor and its handle types) live in the per-runtime driver modules (tokio,smol) or driver crates (compio,smoltcp,embassy). These require a proto storage tier, which every runtime / driver feature enables, so the re-export is gated on having one. Spec for starting a query. - Service
Records compioorembassy-no-atomicorembassyorsmolorsmoltcp-no-atomicorsmoltcportokio - Common, runtime-agnostic protocol types — re-exported from
mdns_protofor ergonomics. Service / query specs, records, names, and update events are identical for every driver, so they live at the crate root; the runtime-specific entry points (the endpoint constructor and its handle types) live in the per-runtime driver modules (tokio,smol) or driver crates (compio,smoltcp,embassy). These require a proto storage tier, which every runtime / driver feature enables, so the re-export is gated on having one. Records advertised by a single registered service. - Service
Spec compioorembassy-no-atomicorembassyorsmolorsmoltcp-no-atomicorsmoltcportokio - Common, runtime-agnostic protocol types — re-exported from
mdns_protofor ergonomics. Service / query specs, records, names, and update events are identical for every driver, so they live at the crate root; the runtime-specific entry points (the endpoint constructor and its handle types) live in the per-runtime driver modules (tokio,smol) or driver crates (compio,smoltcp,embassy). These require a proto storage tier, which every runtime / driver feature enables, so the re-export is gated on having one. Spec for registering a service.
Enums§
- Service
Update compioorembassy-no-atomicorembassyorsmolorsmoltcp-no-atomicorsmoltcportokio - Common, runtime-agnostic protocol types — re-exported from
mdns_protofor ergonomics. Service / query specs, records, names, and update events are identical for every driver, so they live at the crate root; the runtime-specific entry points (the endpoint constructor and its handle types) live in the per-runtime driver modules (tokio,smol) or driver crates (compio,smoltcp,embassy). These require a proto storage tier, which every runtime / driver feature enables, so the re-export is gated on having one. App-level events emitted byService::poll().