Representing Socket Addresses in Swift Using Enums

The API for low-level socket addresses is a strange thing. And it doesn’t get better when used in Swift. So let’s build something to improve the situation.

There’s a concept in C that allows you to write functions that operate on multiple, different types (structs) that share a couple of members at their respective starts. It involves taking pointers to such types and casting them between each other. Should this concept have a fancy name, then I’m not aware of it, but I would guess that it contains the word polymorphism. (If you know a name for this, please let me know!)

The POSIX socket API uses this concept in its representation of socket addresses. For example, connect() takes a pointer to something called sockaddr, but that type is something generic. What we care about are concrete types like sockaddr_in or sockaddr_in6 that represent actual IPv4 or IPv6 addresses.

Here’s what these three types look like in C:

// Generic socket address:
struct sockaddr {
    __uint8_t   sa_len;
    sa_family_t sa_family;
    char        sa_data[14];
};

// Concrete IPv4 socket address:
struct sockaddr_in {
    __uint8_t   sin_len;
    sa_family_t sin_family;
    in_port_t   sin_port;
    struct      in_addr sin_addr;
    char        sin_zero[8];
};

// Concrete IPv6 socket address:
struct sockaddr_in6 {
    __uint8_t       sin6_len;
    sa_family_t     sin6_family;
    in_port_t       sin6_port;
    __uint32_t      sin6_flowinfo;
    struct in6_addr sin6_addr;
    __uint32_t      sin6_scope_id;
};

These three structs are different in their layouts and sizes, but their first two fields are the same: an address length and an address family. Functions like connect() or accept() use this information to change their behavior depending on the concrete needs for the given address.

Users of this API need to provide enough memory to store the concrete address they want to use, e.g. a sockaddr_in6, and then pass a pointer to that, cast to a pointer to the generic sockaddr. This means that the information about how many bytes are available at the address pointed to by any given pointer is neither available at compile time, nor at run time. Therefore, all the functions dealing with such addresses also take an additional parameter for the struct’s length.

In C, using this technique might look like the following code:

// An existing socket:
int socket = ...;

// Provide storage for the IPv6 address and fill in the values somehow:
struct sockaddr_in6 socketAddress;
...

// Pass the concrete IPv6 address to a function that takes a generic socket address:
connect(socket, (struct sockaddr *)&socketAddress, sizeof(socketAddress));

Representing it in Swift

All this is a really long way of saying that it’s cumbersome to use such an API and that we might want to do something to improve this in Swift. We could define a protocol that represents a socket address or a struct that wraps any socket address, but I’ve chosen to use an enum whose cases have associated values to implement this in our SocketWrapper library.

Right now, the library only supports IPv4 and IPv6 addresses, so the enum looks like this:

enum SocketAddress {
    case Version4(address: sockaddr_in)
    case Version6(address: sockaddr_in6)
}

This way, it’s easy to create an instance of this enum if a suitable socket address is already available. Usually we resolve a host and port to an address info (which we wrapped in my previous blog post), so we’ll have an addrinfo. Let’s add an initializer that takes such an addrinfo and uses that to create a SocketAddress:

extension SocketAddress {
    init(addrInfo: addrinfo) {
        switch addrInfo.ai_family {
        case AF_INET:
            self = .Version4(address: UnsafePointer(addrInfo.ai_addr).memory)

        case AF_INET6:
            self = .Version6(address: UnsafePointer(addrInfo.ai_addr).memory)

        default:
            fatalError("Unknown address family")
        }
    }
}

Improved Address Access

Now that we have an instance of SocketAddress, we probably want to call connect(). To do that, we need to access the associated value of the enum case, which is done by pattern matching:

// Assume these exist:
let socket: Int32 = ...
let socketAddress: SocketAddress = ...

if case .Version6(var address) = socketAddress {
    withUnsafePointer(&address) {
        connect(socket, UnsafePointer<sockaddr>($0), socklen_t(sizeof(sockaddr_in6)))
    }
}

This isn’t very pretty. First, we need to pattern match using if case or switch every time we want to get to the associated value. Second, that code isn’t really straightforward, but it boils down to this:

  • Take a pointer to the socket address. In contrast to C, we can’t just take the address of any variable simply by prefixing it with an &, so we’ll have to use withUnsafePointer(). address must be a var so we can use the & operator on it.
  • withUnsafePointer() takes a closure that gets passed in an UnsafePointer to whatever we passed as the first argument, i.e. it’s an UnsafePointer<sockaddr_in6> for the IPv6 case. Note that this is a pointer to the concrete IPv6 address, but connect() wants a pointer to the abstract address. We therefore create an UnsafePointer<sockaddr> with the same memory address.
  • connect() is then called with this UnsafePointer<sockaddr>.

The above code only works for SocketAddress.Version6, i.e. IPv6 addresses. If it should work for IPv4 addresses too we’d need to replace the if case with a switch whereas each case would have the exact same code in it. Fortunately, a small local generic helper function can help us not write too much duplicate code:

extension SocketAddress {
    func callConnect(socket: Int32) {

        // Local helper function:
        func castAndConnect<T>(address: T) {
            var localAddress = address // We need a `var` here for the `&`.
            return withUnsafePointer(&localAddress) {
                Darwin.connect(socket, UnsafePointer<sockaddr>($0), socklen_t(sizeof(T)))
            }
        }

        switch self {
        case .Version4(let address):
            return castAndConnect(address)

        case .Version6(let address):
            return castAndConnect(address)
        }
    }
}

Again, there are a few hoops we have to jump through. We now have the repetitive code bundled up in a local generic function, but the calls to that function must still be written explicitly for each case and each call looks absolutely identical. This is because the compiler needs to see the actual invocation with the concrete types to be able to generate the concrete variants of the castAndConnect() function.

Nonetheless, we now have a method that takes care of providing the socket address as a generic UnsafePointer<sockaddr>, regardless of whether it’s an IPv4 or IPv6 address. The implementation is still very specific to connect(), so maybe we can improve on that to make it more flexible. By taking some inspiration from Swift standard library functions like withUnsafePointer() that take a closure to do the actual work, we end up with something like this:

extension SocketAddress {
    func withSockAddrPointer<Result>(@noescape body: (UnsafePointer<sockaddr>, socklen_t) throws -> Result) rethrows -> Result {

        func castAndCall<T>(address: T, @noescape _ body: (UnsafePointer<sockaddr>, socklen_t) throws -> Result) rethrows -> Result {
            var localAddress = address // We need a `var` here for the `&`.
            return try withUnsafePointer(&localAddress) {
                try body(UnsafePointer<sockaddr>($0), socklen_t(sizeof(T)))
            }
        }

        switch self {
        case .Version4(let address):
            return try castAndCall(address, body)

        case .Version6(let address):
            return try castAndCall(address, body)
        }
    }
}

Now we can improve the code from earlier to take advantage of this new withSockAddrPointer():

// Assume these exist:
let socket: Int32 = ...
let socketAddress: SocketAddress = ...

socketAddress.withSockAddrPointer { sockaddr, length in
    connect(socket, sockaddr, length)
}

Note that the calling code is not tied to any specific address family anymore, i.e. it works regardless of whether socketAddress is an IPv4 or IPv6 address. Nice!

Improved Address Creation

All of this is really useful for a case like connect() where we already have a socket address that we want to connect to. Its size is already known and there’s already storage allocated for it somewhere. But there’s another common case where we don’t know in advance whether we will have an IPv4 or IPv6 address and that is the accept() function, which is called in a server process to accept incoming client sockets’ connections to a server socket. The function’s signature shows that the connecting client’s socket address and its length are returned via out parameters and the function’s return value is the actual socket of the connecting client:

func accept(serverSocket: Int32, clientSocketAddress: UnsafeMutablePointer<sockaddr>, clientSocketAddressLength: UnsafeMutablePointer<socklen_t>) -> Int32

Because a server socket can be available on multiple addresses at the same time, it can be available at IPv4 and IPv6 addresses at the same time. This means a connecting client could either have an IPv4, or an IPv6 address. So how do we know what kind of address we have to allocate memory for and should pass into the function? The answer is yet another type called sockaddr_storage that is guaranteed to be large enough to hold any address there is:

// Assume this exists:
let serverSocket: Int32 = ...

var clientSocketAddress = sockaddr_storage()
var clientSocketAddressLength: socklen_t = 0

let clientSocket = Darwin.accept(serverSocket, &clientSocketAddress, &clientSocketAddressLength)

Just like we implemented withSockAddrPointer() so we don’t have to tie our implementation of SocketAddress to a concrete use case like calling connect(), we don’t want to call accept() directly anywhere in SocketAddress’s implementation. Instead, we’ll pass a closure that does that, just like before. Because this is a use case where we don’t have an existing SocketAddress yet, we’ll have to create a new instance. That sounds like an initializer:

extension SocketAddress {
    init?(@noescape addressProvider: (UnsafeMutablePointer<sockaddr>, UnsafeMutablePointer<socklen_t>) throws -> Void) rethrows {

        var addressStorage = sockaddr_storage()
        var addressStorageLength = socklen_t(sizeofValue(addressStorage))
        try withUnsafeMutablePointers(&addressStorage, &addressStorageLength) {
            try addressProvider(UnsafeMutablePointer<sockaddr>($0), $1)
        }

        switch Int32(addressStorage.ss_family) {
        case AF_INET:
            self = withUnsafePointer(&addressStorage) { .Version4(address: UnsafePointer<sockaddr_in>($0).memory) }

        case AF_INET6:
            self = withUnsafePointer(&addressStorage) { .Version6(address: UnsafePointer<sockaddr_in6>($0).memory) }

        default:
            return nil
        }
    }
}

The addressProvider closure is passed in by the caller and gets a pointer to a sockaddr_storage large enough to hold any socket address, but that pointer is already cast to UnsafeMutablePointer<sockaddr> so it’ll fit the parameter types of functions like accept() (it’s mutable because it will be filled in by the function). After addressProvider returns, the sockaddr_storage should contain information that allows us to know what kind of address we got (the addressStorage.ss_family), so we can pick the appropriate enum value and assign it to self. Should the addressProvider not fill in the necessary information or just do nothing, the initializer simply returns nil.

At the call site, it looks like this:

// Assume this exists:
let serverSocket: Int32 = ...

let clientSocket: Int32
let clientSocketAddress = SocketAddress { sockAddrPointer, sockAddrLength in
    clientSocket = Darwin.accept(serverSocket, sockAddrPointer, sockAddrLength)
}

Conclusion

We now have a single type called SocketAddress that abstracts away the creation and access to POSIX socket addresses and their storage, freeing callers from any details about how different socket addresses are represented. Granted, we have to add a case for every type of address we want to support, but that usually doesn’t vary much over time (there are other socket types than IP sockets, e.g. UNIX domain sockets).