QSocket: Addresses

This thread has been locked by a moderator.

IMPORTANT If you haven’t yet read Calling BSD Sockets from Swift, do that first.

The biggest problem with calling BSD Sockets from Swift is how to represent an address. The C API works in terms of struct sockaddr *, which is a ‘base class’ with various ‘subclasses’, like struct sockaddr_in *. C lets you [1] freely cast between these, but that’s harder in Swift. My solution for this — and remember that one of my key goals is to create a wrapper that’s good for test projects — is to represent addresses as strings.

I start by defining a namespace for this stuff:

/// A namespace for helpers that work with BSD Sockets addresses.
///
/// These convert between IP address strings and the `sockaddr` pointers used by
/// the BSD Sockets API. For example, here’s how you can call `connect` with an
/// IP address and string.
///
/// ```swift
/// let success = try QSockAddr.withSockAddr(address: "1.2.3.4", port: 12345) { sa, saLen in
///     connect(fd, sa, saLen) >= 0
/// }
/// ```
///
/// This example calls ``withSockAddr(address:port:_:)``, which is what you use
/// when passing an address into BSD Sockets. There’s also
/// ``fromSockAddr(sa:saLen:)``, to use when getting an address back from BSD
/// Sockets.
///
/// > important: Representing addresses as strings is potentially very
/// inefficient.  For example, if you were to wrap the BSD Sockets `sendto` call
/// in this way, you would end up doing a string-to-address conversion every
/// time you sent a datagram!  However, it _is_ very convenient, making it perfect
/// for small test projects, wrapping weird low-level APIs, and so on.
///
/// Keep in mind that I rarely use BSD Sockets for _networking_ these days.
/// Apple platforms have better networking APIs; see TN3151 [Choosing the right
/// networking API][tn3151] for the details.
///
/// [tn3151]: <[TN3151: Choosing the right networking API | Apple Developer Documentation](https://developer.apple.com/documentation/technotes/tn3151-choosing-the-right-networking-api)>

public enum QSockAddr {
}

I then extend it with various helpers. The first is for calling a routine that takes a sockaddr input:

extension QSockAddr {
    
    /// Calls a closure with a socket address and length.
    ///
    /// Use this to pass an address in to a BSD Sockets call. For example:
    ///
    /// ```swift
    /// let success = try QSockAddr.withSockAddr(address: "1.2.3.4", port: 12345) { sa, saLen in
    ///     connect(fd, sa, saLen) >= 0
    /// }
    /// ```
    ///
    /// - Parameters:
    ///   - address: The address as a string. This can be either an IPv4 or IPv6
    ///   address, in any format accepted by `getaddrinfo` when the
    ///   `AI_NUMERICHOST` flag is set.
    ///   - port: The port number.
    ///   - body: A closure to call with the corresponding `sockaddr` pointer
    ///   and length.
    /// - Returns: The value returned by that closure.

    public static func withSockAddr<ReturnType>(
        address: String,
        port: UInt16,
        _ body: (_ sa: UnsafePointer<sockaddr>, _ saLen: socklen_t) throws -> ReturnType
    ) throws -> ReturnType {
        var addrList: UnsafeMutablePointer<addrinfo>? = nil
        var hints = addrinfo()
        hints.ai_flags = AI_NUMERICHOST | AI_NUMERICSERV
        let err = getaddrinfo(address, "\(port)", &hints, &addrList)
        guard err == 0 else { throw QSockAddr.NetDBError(code: err) }
        guard let addr = addrList else { throw QSockAddr.NetDBError(code: EAI_NODATA) }
        defer { freeaddrinfo(addrList) }
        return try body(addr.pointee.ai_addr, addr.pointee.ai_addrlen)
    }
}

The second is for calling a routine that returns a sockaddr:

extension QSockAddr {
    
    /// Creates an address by calling a closure to fill in a socket address and
    /// length.
    ///
    /// Use this to get an address back from a BSD Sockets call. For example:
    ///
    /// ```swift
    /// let peer = try QSockAddr.fromSockAddr() { sa, saLen in
    ///     getpeername(fd, sa, &saLen) >= 0
    /// }
    /// guard peer.result else { … something went wrong … }
    /// … use peer.address and peer.port …
    /// ```
    ///
    /// - Parameter body: The closure to call. It passes this a mutable pointer
    /// to a `sockaddr` and an `inout` length. The closure is expected to
    /// populate that memory with an IPv4 or IPv6 address, or throw an error.
    /// - Returns: A tuple containing the closure result, the address string,
    /// and the port.

    public static func fromSockAddr<ReturnType>(_ body: (_ sa: UnsafeMutablePointer<sockaddr>, _ saLen: inout socklen_t) throws -> ReturnType) throws -> (result: ReturnType, address: String, port: UInt16) {
        var ss = sockaddr_storage()
        var saLen = socklen_t(MemoryLayout<sockaddr_storage>.size)
        return try withUnsafeMutablePointer(to: &ss) {
            try $0.withMemoryRebound(to: sockaddr.self, capacity: 1) { sa in
                let result = try body(sa, &saLen)
                let (address, port) = try fromSockAddr(sa: sa, saLen: saLen)
                return (result, address, port)
            }
        }
    }
}

And the third is for when you start with a sockaddr pointer:

extension QSockAddr {
    
    /// Creates an address from an address pointer and length.
    ///
    /// Use this when you have an existing `sockaddr` pointer, for example, when
    /// working with `getifaddrs`.
    ///
    /// - Parameters:
    ///   - sa: The address pointer
    ///   - saLen: The address length.
    /// - Returns: A tuple containing the address string, and the port.

    public static func fromSockAddr(sa: UnsafeMutablePointer<sockaddr>, saLen: socklen_t) throws -> (address: String, port: UInt16) {
        var host = [CChar](repeating: 0, count: Int(NI_MAXHOST))
        var serv = [CChar](repeating: 0, count: Int(NI_MAXSERV))
        let err = getnameinfo(sa, saLen, &host, socklen_t(host.count), &serv, socklen_t(serv.count), NI_NUMERICHOST | NI_NUMERICSERV)
        guard err == 0 else { throw QSockAddr.NetDBError(code: err) }
        guard let port = UInt16(String(cString: serv)) else { throw QSockAddr.NetDBError(code: EAI_SERVICE) }
        return (String(cString: host), port)
    }
}

Finally, these routines rely on this error type:

extension QSockAddr {
    
    /// Wraps an error coming from the DNS subsytem.
    ///
    /// The code values correspond to `EAI_***` in `<netdb.h>`.

    struct NetDBError: Error {
        var code: CInt
    }
}

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] Or does it? There’s some debate as to whether the BSD Sockets API is legal C (-:

Up vote post of eskimo
453 views