Name Resolution

The resolver class performs asynchronous DNS lookups, converting hostnames to IP addresses. It wraps the system’s getaddrinfo() function with an asynchronous interface.

Code snippets assume:

#include <boost/corosio/resolver.hpp>
#include <boost/capy/read.hpp>
#include <boost/capy/write.hpp>
namespace corosio = boost::corosio;

Overview

corosio::resolver r(ioc);
auto [ec, results] = co_await r.resolve("www.example.com", "https");

for (auto const& entry : results)
{
    auto ep = entry.get_endpoint();
    std::cout << ep.v4_address().to_string() << ":" << ep.port() << "\n";
}

Construction

corosio::io_context ioc;
corosio::resolver r(ioc);  // From execution context

Resolving Names

Basic Resolution

auto [ec, results] = co_await r.resolve("www.example.com", "80");

The host can be:

  • A hostname: "www.example.com"

  • An IPv4 address string: "192.168.1.1"

  • An IPv6 address string: "::1" or "2001:db8::1"

The service can be:

  • A service name: "http", "https", "ssh"

  • A port number string: "80", "443", "22"

  • An empty string: "" (returns port 0)

With Flags

auto [ec, results] = co_await r.resolve(
    "www.example.com",
    "https",
    corosio::resolve_flags::address_configured);

Resolve Flags

Flag Description

none

No special behavior (default)

passive

Return addresses suitable for bind() (server use)

numeric_host

Host is a numeric address string, don’t perform DNS lookup

numeric_service

Service is a port number string, don’t look up service name

address_configured

Only return IPv4 if the system has IPv4 configured, same for IPv6

v4_mapped

Intended to return IPv4-mapped IPv6 addresses when no IPv6 addresses are found. Has no effect in the current implementation (see note below).

all_matching

Intended to combine with v4_mapped to return all matching IPv4 and IPv6 addresses. Has no effect in the current implementation (see note below).

v4_mapped and all_matching are currently inert. The resolver always queries with ai_family = AF_UNSPEC, but the underlying AI_V4MAPPED and AI_ALL behavior only takes effect for AF_INET6-family queries.

Flags can be combined:

auto flags =
    corosio::resolve_flags::numeric_host |
    corosio::resolve_flags::numeric_service;

auto [ec, results] = co_await r.resolve("127.0.0.1", "8080", flags);

Working with Results

resolver_results is an alias for std::vector<resolver_entry>, so it supports the full std::vector interface (iteration, size(), empty(), indexing, and so on):

using resolver_results = std::vector<resolver_entry>;
for (auto const& entry : results)
{
    corosio::endpoint ep = entry.get_endpoint();

    if (ep.is_v4())
        std::cout << "IPv4: " << ep.v4_address().to_string();
    else
        std::cout << "IPv6: " << ep.v6_address().to_string();

    std::cout << ":" << ep.port() << "\n";
}

Copying a resolver_results deep-copies every entry, and each entry owns two std::string query names. When handing the results to a sink that takes the range by value — such as corosio::connect — pass an rvalue (std::move(results)) or use the iterator-based overload (connect(s, results.begin(), results.end())) to avoid the copy.

resolver_entry Interface

class resolver_entry
{
public:
    corosio::endpoint get_endpoint() const;

    // Implicit conversion to endpoint
    operator corosio::endpoint() const;

    // Query strings used in the resolution
    std::string const& host_name() const;
    std::string const& service_name() const;
};

Connecting to Resolved Addresses

Try each address until one works:

capy::task<void> connect_to_service(
    corosio::io_context& ioc,
    std::string_view host,
    std::string_view service)
{
    corosio::resolver r(ioc);
    auto [resolve_ec, results] = co_await r.resolve(host, service);

    if (resolve_ec)
        throw std::system_error(resolve_ec);

    if (results.empty())
        throw std::runtime_error("No addresses found");

    corosio::tcp_socket sock(ioc);
    sock.open();

    std::error_code last_error;
    for (auto const& entry : results)
    {
        auto [ec] = co_await sock.connect(entry.get_endpoint());
        if (!ec)
            co_return;  // Connected successfully

        last_error = ec;
        sock.close();
        sock.open();
    }

    throw std::system_error(last_error);
}

Cancellation

cancel()

Cancel pending resolution:

r.cancel();

The in-flight resolution completes with capy::cond::canceled (the underlying value is capy::error::canceled). Match it portably with ec == capy::cond::canceled rather than comparing against a specific enumerator.

Stop Token Cancellation

Resolver operations support stop token cancellation through the affine protocol.

Error Handling

Resolution failures do not surface as named DNS error codes. The underlying getaddrinfo() EAI_* errors are mapped to generic std::errc values:

Failure Resulting std::error_code

Host not found (EAI_NONAME)

std::errc::no_such_device_or_address

Unknown service (EAI_SERVICE)

std::errc::invalid_argument

Temporary failure (EAI_AGAIN)

std::errc::resource_unavailable_try_again

Cancellation is reported through capy’s category rather than std::errc. Match it with:

if (ec == capy::cond::canceled)
{
    // resolution was cancelled
}

Move Semantics

Resolvers are move-only:

corosio::resolver r1(ioc);
corosio::resolver r2 = std::move(r1);  // OK

corosio::resolver r3 = r2;  // Error: deleted copy constructor
After a move, the destination uses the source’s execution context.

Thread Safety

Operation Thread Safety

Distinct resolvers

Safe from different threads

Same resolver

NOT safe for concurrent operations

Single-Inflight Constraint

Each resolver can only have ONE resolve operation in progress at a time. Starting a second resolve() while the first is still pending results in undefined behavior.

// CORRECT: Sequential resolves on same resolver
auto [ec1, r1] = co_await resolver.resolve("host1", "80");
auto [ec2, r2] = co_await resolver.resolve("host2", "80");

// CORRECT: Parallel resolves with separate resolver instances
corosio::resolver r1(ioc), r2(ioc);
// In separate coroutines:
auto [ec1, res1] = co_await r1.resolve("host1", "80");
auto [ec2, res2] = co_await r2.resolve("host2", "80");

// WRONG: Concurrent resolves on same resolver - UNDEFINED BEHAVIOR
auto f1 = resolver.resolve("host1", "80");
auto f2 = resolver.resolve("host2", "80");  // BAD: overlaps with f1

If you need to resolve multiple hostnames concurrently, create a separate resolver instance for each.

Example: HTTP Client with Resolution

capy::task<void> http_get(
    corosio::io_context& ioc,
    std::string_view hostname)
{
    // Resolve hostname
    corosio::resolver r(ioc);
    auto [resolve_ec, results] = co_await r.resolve(hostname, "80");

    if (resolve_ec)
    {
        std::cerr << "Resolution failed: " << resolve_ec.message() << "\n";
        co_return;
    }

    // Connect to first address
    corosio::tcp_socket sock(ioc);
    sock.open();

    for (auto const& entry : results)
    {
        auto [ec] = co_await sock.connect(entry);
        if (!ec)
            break;
    }

    if (!sock.is_open())
    {
        std::cerr << "Failed to connect\n";
        co_return;
    }

    // Send HTTP request
    std::string request =
        "GET / HTTP/1.1\r\n"
        "Host: " + std::string(hostname) + "\r\n"
        "Connection: close\r\n"
        "\r\n";

    if (auto [ec, n] = co_await capy::write(
            sock, capy::const_buffer(request.data(), request.size())); ec)
        throw std::system_error(ec);

    // Read response
    std::string response;
    co_await capy::read(sock, response);

    std::cout << response << "\n";
}

Platform Notes

The resolver uses the system’s getaddrinfo() function. On most platforms, this is a blocking call executed on a thread pool to avoid blocking the I/O context.

Because resolution runs on a thread pool, it requires a multi-threaded io_context. When the io_context is constructed single-threaded (a concurrency_hint of 1), resolve() completes with std::errc::operation_not_supported and never performs a lookup.

Next Steps