Representing Socket Addresses in Swift Using Enums
This blog post is the fourth in a five part series about POSIX Sockets in Swift and our SocketWrapper Library:
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 (struct
s) 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 struct
s 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 case
s 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 usewithUnsafePointer()
.address
must be avar
so we can use the&
operator on it. withUnsafePointer()
takes a closure that gets passed in anUnsafePointer
to whatever we passed as the first argument, i.e. it’s anUnsafePointer<sockaddr_in6>
for the IPv6 case. Note that this is a pointer to the concrete IPv6 address, butconnect()
wants a pointer to the abstract address. We therefore create anUnsafePointer<sockaddr>
with the same memory address.connect()
is then called with thisUnsafePointer<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).