Skip to main content

Crate hick

Crate hick 

Source
Expand description

hick

Batteries-included, runtime-agnostic mDNS / DNS-SD for Rust — a Sans-I/O protocol core with pluggable async drivers.

github LoC Build codecov

docs.rs crates.io crates.io license

§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-proto as 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, or compio (thread-per-core) with no change to protocol behavior.
  • no_std and bare-metal. The core runs on alloc — or heapless, with no allocator at all — and hick-smoltcp / hick-embassy bring 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)]; unsafe is confined to the platform socket and control-message plumbing.
  • Observable, à la carte. Opt into tracing spans, metrics counters, pollable stats, or defmt on 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:

CrateRole
hickthis crate — batteries-included facade (core + default driver)
mdns-protoSans-I/O protocol state machines (no_std-capable)
hick-udpmulticast UDP socket setup
hick-reactorruntime-agnostic async driver (tokio & smol)
hick-compiocompio (thread-per-core) async driver
hick-smoltcpbare-metal mDNS engine over smoltcp (no_std + alloc)
hick-embassyembassy async driver, built on hick-smoltcp (no_std + alloc)

§Installation

[dependencies]
hick = "0.1" # tokio runtime by default

For 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 the tokio runtime via hick-reactor.
  • smol — drive the smol runtime.
  • compio — drive the compio (thread-per-core) runtime via hick-compio.
  • smoltcp — expose the bare-metal hick-smoltcp engine (no_std + alloc).
  • embassy — expose the hick-embassy async driver (no_std + alloc).
  • smoltcp-no-atomic / embassy-no-atomic — the same engine / driver on the no-atomic tier, for cores without native atomic CAS (Cortex-M0+ / thumbv6m / RP2040). Refcounted Name / rdata use portable-atomic (clone via a critical-section impl your binary provides). Pick exactly one tier per engine.
  • tracing — forward structured tracing spans/events from every enabled driver and the proto core.
  • stats — enable no_std-safe atomic counters; read a snapshot via endpoint.stats().
  • metrics — bridge counters to the metrics facade (Prometheus/StatsD exporters). Implies stats.
  • defmt — forward defmt log 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;compio
pub use hick_smoltcp as smoltcp;smoltcp-no-atomic or smoltcp
pub use hick_embassy as embassy;embassy-no-atomic or embassy

Modules§

smolsmol
smol-pinned mDNS driver — same shape as tokio.
tokiotokio
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§

CollectedAnswercompio or embassy-no-atomic or embassy or smol or smoltcp-no-atomic or smoltcp or tokio
Common, runtime-agnostic protocol types — re-exported from mdns_proto for 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.
Namecompio or embassy-no-atomic or embassy or smol or smoltcp-no-atomic or smoltcp or tokio
Common, runtime-agnostic protocol types — re-exported from mdns_proto for 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).
QuerySpeccompio or embassy-no-atomic or embassy or smol or smoltcp-no-atomic or smoltcp or tokio
Common, runtime-agnostic protocol types — re-exported from mdns_proto for 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.
ServiceRecordscompio or embassy-no-atomic or embassy or smol or smoltcp-no-atomic or smoltcp or tokio
Common, runtime-agnostic protocol types — re-exported from mdns_proto for 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.
ServiceSpeccompio or embassy-no-atomic or embassy or smol or smoltcp-no-atomic or smoltcp or tokio
Common, runtime-agnostic protocol types — re-exported from mdns_proto for 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§

ServiceUpdatecompio or embassy-no-atomic or embassy or smol or smoltcp-no-atomic or smoltcp or tokio
Common, runtime-agnostic protocol types — re-exported from mdns_proto for 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 by Service::poll().