dns.h — Asynchronous DNS Resolution

Introduction

dns.h provides asynchronous DNS resolution by offloading getaddrinfo() to the event loop's thread pool. The completion callback is always invoked on the event loop thread, maintaining xKit's single-threaded callback model. Queries can be cancelled before the callback fires.

Design Philosophy

  1. Thread-Pool Offloadgetaddrinfo() is a blocking POSIX call. Rather than introducing a dedicated DNS thread, xnet reuses the event loop's existing thread pool via xEventLoopSubmit().

  2. Event-Loop-Thread Callbacks — The done callback runs on the event loop thread, so user code never needs synchronization. This is consistent with every other callback in xKit.

  3. Linked-List Result — Resolved addresses are returned as a linked list of xDnsAddr nodes, preserving the full getaddrinfo() result (family, socktype, protocol) for each address.

  4. Cancellation SupportxDnsCancel() sets an atomic flag. If the worker has already finished, the done callback silently discards the result instead of invoking the user callback.

  5. IP Literal Fast Path — If the hostname is an IPv4 or IPv6 literal, AI_NUMERICHOST is set automatically, skipping the actual DNS lookup.

Architecture

sequenceDiagram
    participant App as Application
    participant EL as Event Loop Thread
    participant TP as Thread Pool Worker

    App->>EL: xDnsResolve(loop, "example.com", ...)
    EL->>TP: xEventLoopSubmit(dns_work_fn)
    Note over TP: getaddrinfo() (blocking)
    TP-->>EL: dns_done_fn(result)
    alt Not cancelled
        EL->>App: callback(result, arg)
    else Cancelled
        EL->>EL: xDnsResultFree(result)
    end

Implementation Details

Internal Request Lifecycle

stateDiagram-v2
    [*] --> Created: xDnsResolve()
    Created --> Queued: xEventLoopSubmit()
    Queued --> Working: Thread pool picks up
    Working --> Done: getaddrinfo() returns
    Done --> Delivered: callback invoked
    Done --> Discarded: cancelled flag set

    Queued --> Cancelled: xDnsCancel()
    Working --> Cancelled: xDnsCancel()
    Cancelled --> Discarded: done_fn checks flag

    Delivered --> [*]: request freed
    Discarded --> [*]: request freed

Error Mapping

getaddrinfo() returns EAI_* codes. These are mapped to xKit error codes:

EAI CodexErrnoMeaning
0 (success)xErrno_OkResolution succeeded
EAI_NONAMExErrno_DnsNotFoundHost not found
EAI_AGAINxErrno_DnsTempFailTemporary failure
EAI_MEMORYxErrno_NoMemoryOut of memory
OtherxErrno_DnsErrorGeneric DNS error

IP Literal Detection

Before calling getaddrinfo(), the worker checks if the hostname is an IP literal using inet_pton(). If it is, AI_NUMERICHOST is added to the hints, which tells getaddrinfo() to skip DNS lookup entirely.

// Pseudocode
if (inet_pton(AF_INET, hostname, buf) == 1 ||
    inet_pton(AF_INET6, hostname, buf) == 1) {
    hints.ai_flags |= AI_NUMERICHOST;
}

API Reference

Core Functions

FunctionSignatureDescription
xDnsResolvexDnsQuery xDnsResolve(xEventLoop loop, const char *hostname, const char *service, const struct addrinfo *hints, xDnsCallback callback, void *arg)Start async DNS resolution
xDnsCancelvoid xDnsCancel(xEventLoop loop, xDnsQuery query)Cancel a pending query
xDnsResultFreevoid xDnsResultFree(xDnsResult *result)Free a resolution result

Types

TypeDescription
xDnsQueryOpaque handle to a pending query
xDnsResultResolution result: error + addrs linked list
xDnsAddrSingle resolved address node
xDnsCallbackvoid (*)(xDnsResult *result, void *arg)

xDnsResult Fields

FieldTypeDescription
errorxErrnoxErrno_Ok on success
addrsxDnsAddr *Linked list of addresses, or NULL

xDnsAddr Fields

FieldTypeDescription
addrstruct sockaddr_storageResolved socket address
addrlensocklen_tLength of the address
familyintAF_INET or AF_INET6
socktypeintSOCK_STREAM or SOCK_DGRAM
protocolintIPPROTO_TCP or IPPROTO_UDP
nextxDnsAddr *Next address, or NULL

Parameter Details for xDnsResolve

ParameterRequiredDescription
loopYesEvent loop (must not be NULL)
hostnameYesHostname or IP literal (non-empty)
serviceNoPort string (e.g. "443") or NULL
hintsNoaddrinfo hints; NULL defaults to AF_UNSPEC + SOCK_STREAM
callbackYesCompletion callback (must not be NULL)
argNoUser argument forwarded to callback

Returns a xDnsQuery handle, or NULL on invalid arguments.

Usage Examples

Basic Resolution

#include <stdio.h>
#include <arpa/inet.h>
#include <xbase/event.h>
#include <xnet/dns.h>

static void on_resolved(xDnsResult *result, void *arg) {
    xEventLoop loop = (xEventLoop)arg;

    if (result->error != xErrno_Ok) {
        fprintf(stderr, "DNS failed: %d\n", result->error);
        xDnsResultFree(result);
        xEventLoopStop(loop);
        return;
    }

    for (xDnsAddr *a = result->addrs; a; a = a->next) {
        char buf[INET6_ADDRSTRLEN];
        if (a->family == AF_INET) {
            struct sockaddr_in *sin = (struct sockaddr_in *)&a->addr;
            inet_ntop(AF_INET, &sin->sin_addr, buf, sizeof(buf));
        } else {
            struct sockaddr_in6 *sin6 = (struct sockaddr_in6 *)&a->addr;
            inet_ntop(AF_INET6, &sin6->sin6_addr, buf, sizeof(buf));
        }
        printf("  %s (family=%d)\n", buf, a->family);
    }

    xDnsResultFree(result);
    xEventLoopStop(loop);
}

int main(void) {
    xEventLoop loop = xEventLoopCreate();

    xDnsResolve(loop, "example.com", "443", NULL, on_resolved, loop);
    xEventLoopRun(loop);
    xEventLoopDestroy(loop);
    return 0;
}

IPv4-Only Resolution

struct addrinfo hints = {0};
hints.ai_family   = AF_INET;
hints.ai_socktype = SOCK_STREAM;

xDnsResolve(loop, "example.com", "80", &hints, on_resolved, loop);```

### Cancelling a Query

```c
xDnsQuery q = xDnsResolve(loop, "slow.example.com", NULL, NULL, on_resolved, NULL);
// Cancel immediately — callback will NOT fire
xDnsCancel(loop, q);

IP Literal (No DNS Lookup)

// Resolves instantly via AI_NUMERICHOST
xDnsResolve(loop, "127.0.0.1", "8080", NULL, on_resolved, loop);

xDnsResolve(loop, "::1", "8080", NULL, on_resolved, loop);

Thread Safety

OperationThread Safety
xDnsResolve()Call from event loop thread only
xDnsCancel()Call from event loop thread only
xDnsResultFree()Call from any thread (result is owned)
xDnsCallbackAlways invoked on event loop thread

Error Handling

ScenarioBehavior
NULL loop, hostname, or callbackReturns NULL (no query created)
Empty hostnameReturns NULL
malloc failureReturns NULL
getaddrinfo() failureCallback receives result->error != xErrno_Ok
Cancelled queryCallback is not invoked; result is freed internally

Best Practices

  • Always call xDnsResultFree() in your callback. The callee owns the result.
  • Check result->error before iterating addrs. On failure, addrs is NULL.
  • Use xDnsCancel() for cleanup. If you destroy the object that owns the callback context, cancel the query first to prevent a use-after-free.
  • Pass NULL hints for typical use. The defaults (AF_UNSPEC + SOCK_STREAM) cover most HTTP/WebSocket connection scenarios.
  • xDnsCancel(loop, NULL) is safe — it's a no-op, so you don't need to guard against NULL handles.