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 (-: