Like @martinlau mentioned, I’d like to nominate swift-endpoint.
swift-endpoint API is fairly complete as-is, but it will end up needing some adjustments. There are some things we'll need to discuss and some decisions to be made. I can explain further if needed.
Click for discussion
- Should there be a
Domainmodule at all? Like mentioned in the original post. Domainmodule depends onByteBuffer.[UInt8]can do fine, but it'll be a performance hit for swift-nio-dependant libraries which will end up copying aByteBufferslice into a new[UInt8]array.- There is a currently-unused trait to switch between
[UInt8]andByteBuffer. That's also one way to go.
DomainNamecomes with IDN support for non-ascii domain names. It costs a bit of binary size due to the vendored IDNA mappings table, and soon NFC tables (like 200KB I'd say off-hand, not sure). There could possibly be a trait to disable it since IDNs are extremely rare (top 50K Cloudflare domains only has 13 of them).- What availability we'd want this package to have?
- Big question is macOS 15. The package currently contains an
UnsignedInteger128type consisting of 2UInt64s because ofUInt128's requirement of macOS 15. - Dropping macOS 15 might be a big hit on adoption in existing packages.
UnsignedInteger128is almost identical toUInt128, API-wise and behavior wise (there are tests, as well as crash-tests aka exit-tests to ensure). It has a few hand-written impls that I know can be made more performant, but for now my policy has been to let's just discourage usingUnsignedInt128; hence the relatively ugly name. Realistically, anyone can get the performance by simply doingUInt128(myUnsignedInt128)before operating on the value. The performance is not bad to be clear but yeah.
- Big question is macOS 15. The package currently contains an
unixDomainSocketAddressis waiting for theFilePathtype. Theoretically we could try to hide the impl and provideStringAPIs for now.- IPv8 looks to be imminent. Should we at least have stubs for it? since an enum like
AnyIPAddressis a "frozen" enum (not in the ABI-stability meaning of it), not a non-exhaustive one, and we can't add a new case to it without a semver-major bump in the future. - I do recognize IPv8's adoption around the world will take a few years at best anyway, but considering a foundational package like swift-endpoint won't be able to do major releases easily, we should at least plan for it (a major bump might end up breaking too many user codes that breaking changes even in a major bump would be hard to justify, see swift-nio for example).
Furthermore:
- I plan to add fuzz-testing to swift-endpoint as well soon.
- The package is pretty complete on having unit tests, but in such a fundamental package we can't afford any risks.
- The package does also use a fair bit of "unsafe" / "unchecked" constructs for performance reasons, so that's another reason. Although again as far as I can tell there are no security holes, but well, that doesn't mean there are actually no security holes, just that I can see none.
- The package contains a pretty custom benchmark CI so we'll need to see about that if it is to be moved to swiftlang. Currently it uses a custom fork of ordo-one/benchmark as well.
Design
| Module | Contents |
|---|---|
IPAddress |
IPv4Address, IPv6Address, AnyIPAddress, CIDR, UnsignedInteger128 |
Domain |
DomainName, DomainName.Label |
DomainIPAddressCompat |
DomainName ⇄ IP address conversions |
Endpoint |
ConnectionTarget, Port + exports all of the above |
API surface
Here are all the public APIs (LLM generated, with my edits where needed).
Click to show
Assume big-endian order unless explicitly specified.
IPv4Address
A 32-bit IPv4 address stored as a UInt32.
public struct IPv4Address: Sendable, Hashable {
public var address: UInt32
}
Click for IPv4Address full API
extension IPv4Address {
public static var size: Int { get } // 4
/// big-endian order
public var bytes: (UInt8, UInt8, UInt8, UInt8) { get }
public init(_: UInt32)
/// big-endian order
public init(_: UInt8, _: UInt8, _: UInt8, _: UInt8)
// RFC-classified predicates
public var isLoopback: Bool { get }
public var isMulticast: Bool { get }
public var isLinkLocal: Bool { get }
public var isUnspecified: Bool { get }
public var isBroadcast: Bool { get }
public var isPrivate: Bool { get }
public var isShared: Bool { get }
public var isDocumentation: Bool { get }
}
extension IPv4Address: ExpressibleByIntegerLiteral {
public init(integerLiteral: UInt32)
}
extension IPv4Address: CustomStringConvertible, LosslessStringConvertible {
public var description: String { get } // "127.0.0.1"
public init?(_: String)
public init?(_: Substring)
}
extension IPv4Address {
// Allocation-free span parsing/serialization
public init?(parsing: Span<UInt8>)
public func serialize(into: inout OutputSpan<UInt8>) -> Bool
@available(SwiftStdlib 6.2, *)
public init?(textualRepresentation: UTF8Span)
public init?(textualRepresentation: Span<UInt8>)
// C interop
public init?(cString: UnsafePointer<CChar>)
public func withCString<Result>(_: (Span<CChar>) throws -> Result) rethrows -> Result
// Cross-type conversions
public init?(exactly: AnyIPAddress)
public init?(ipv6: IPv6Address) // extracts IPv4-mapped IPv6
}
IPv6Address
A 128-bit IPv6 address. Serialization follows RFC 5952 canonical form; parsing accepts bracketed forms and RFC 4291 IPv4-mapped addresses.
public struct IPv6Address: Sendable, Hashable {
public var address: UnsignedInteger128
}
Click for IPv6Address full API
extension IPv6Address {
public static var size: Int { get } // 16
public var bytes: (UInt8, /* … 16 total … */ UInt8) { get }
public var segments: (UInt16, /* … 8 total … */ UInt16) { get }
public init(_: UnsignedInteger128)
public init(_: UInt16, /* … 8 UInt16 segments … */ _: UInt16)
public init(_: UInt8, /* … 16 UInt8 bytes … */ _: UInt8)
@available(SwiftStdlib 6.0, *)
@_disfavoredOverload public init(_: UInt128)
// RFC-classified predicates
public var isLoopback: Bool { get }
public var isMulticast: Bool { get }
public var isLinkLocalUnicast: Bool { get }
public var isUnspecified: Bool { get }
public var isUniqueLocal: Bool { get }
public var isDocumentation: Bool { get }
}
@available(SwiftStdlib 6.0, *)
extension IPv6Address: ExpressibleByIntegerLiteral {
public init(integerLiteral: UInt128)
}
extension IPv6Address: CustomStringConvertible, LosslessStringConvertible {
public var description: String { get } // "[2001:db8:85a3::100]"
public init?(_: String)
public init?(_: Substring)
}
extension IPv6Address {
public init?(parsing: Span<UInt8>)
public func serialize(into: inout OutputSpan<UInt8>) -> Bool
@available(SwiftStdlib 6.2, *)
public init?(textualRepresentation: UTF8Span)
public init?(textualRepresentation: Span<UInt8>)
public init?(cString: UnsafePointer<CChar>)
public func withCString<Result>(_: (Span<CChar>) throws -> Result) rethrows -> Result
public init?(exactly: AnyIPAddress)
public init(ipv4: IPv4Address) // IPv4-mapped IPv6
}
AnyIPAddress
A tagged union of the two address families that auto-detects on parse.
public enum AnyIPAddress: Sendable, Hashable {
case v4(IPv4Address)
case v6(IPv6Address)
}
Click for AnyIPAddress full API
extension AnyIPAddress {
public var ipv4Value: IPv4Address? { get }
public var ipv6Value: IPv6Address? { get }
public var isIPv4: Bool { get }
public var isIPv6: Bool { get }
public var isLoopback: Bool { get }
public var isMulticast: Bool { get }
public var isContiguous: Bool { get }
}
extension AnyIPAddress: CustomStringConvertible, LosslessStringConvertible {
public var description: String { get }
public init?(_: String)
public init?(_: Substring)
}
extension AnyIPAddress {
@available(SwiftStdlib 6.2, *)
public init?(textualRepresentation: UTF8Span)
public init?(textualRepresentation: Span<UInt8>)
public init?(cString: UnsafePointer<CChar>)
public func withCString<Result>(_: (Span<CChar>) throws -> Result) rethrows -> Result
}
CIDR
A generic CIDR block over either address family, storing a prefix and derived mask.
public struct CIDR<IPAddressType: _IPAddressProtocol>: Sendable {
public let prefix: IPAddressType
public let mask: IPAddressType
}
Click for CIDR full API
extension CIDR {
public typealias AddressValueType = IPAddressType.AddressValueType
public var prefixLength: Int { get }
public var networkAddress: IPAddressType { get }
public init(prefix: IPAddressType, prefixLength: Int)
public init?(prefix: IPAddressType, mask: IPAddressType)
public init(prefix: IPAddressType, uncheckedMask: IPAddressType)
public func contains(_: IPAddressType) -> Bool
public func contains(_: AnyIPAddress) -> Bool
}
extension CIDR: Hashable {}
extension CIDR: CustomStringConvertible, LosslessStringConvertible {
public var description: String { get } // "127.0.0.0/8"
public init?(_: String)
public init?(_: Substring)
}
// Well-known blocks
extension CIDR<IPv4Address> {
public static var loopback: Self { get } // 127.0.0.0/8
public static var multicast: Self { get }
public static var linkLocal: Self { get }
public static var broadcast: Self { get }
public static var shared: Self { get }
public static var unspecified: Self { get }
}
extension CIDR<IPv6Address> {
public static var loopback: Self { get } // ::1/128
public static var multicast: Self { get }
public static var linkLocalUnicast: Self { get }
public static var ipv4Mapped: Self { get }
public static var uniqueLocal: Self { get }
public static var documentation: Self { get }
public static var unspecified: Self { get }
}
UnsignedInteger128
A backport shim used as IPv6Address.address on toolchains without UInt128. It provides the full fixed-width-integer surface and interoperates with UInt128 where available.
public struct UnsignedInteger128: Sendable, Hashable, BitwiseCopyable {
public init(_low: UInt64, _high: UInt64)
// Conforms to: Comparable, AdditiveArithmetic, Numeric, BinaryInteger,
// FixedWidthInteger, UnsignedInteger, Strideable, Codable,
// CustomStringConvertible, ExpressibleByIntegerLiteral (SwiftStdlib 6.0+)
}
DomainName
A DNS-wire-format domain name with Unicode-17 IDNA support. Iterating yields labels.
public struct DomainName: Sendable, Hashable, Sequence,
CustomStringConvertible, CustomDebugStringConvertible {
public var isFQDN: Bool
}
Click for DomainName full API
extension DomainName {
public static var maxLength: UInt8 { get } // 255
public static var maxLabelLength: UInt8 { get } // 63
public static var root: Self { get }
public var encodedLength: Int { get }
public var labelsCount: Int { get }
public var isRoot: Bool { get }
public var isWildcard: Bool { get }
@available(SwiftStdlib 5.1, *)
public init(_: String, idnaConfiguration: IDNA.Configuration = .default) throws
@available(SwiftStdlib 5.1, *)
public init(_: Substring, idnaConfiguration: IDNA.Configuration = .default) throws
@available(SwiftStdlib 6.2, *)
public init(textualRepresentation: UTF8Span,
idnaConfiguration: IDNA.Configuration = .default) throws
public var description: String { get } // Unicode, e.g. "新华网.中国"
public var debugDescription: String { get } // A-label, e.g. "xn--xkrr14bows.xn--fiqs8s"
@available(SwiftStdlib 5.1, *)
public func description(format: DescriptionFormat, options: DescriptionOptions = []) -> String
// Containment
public func isSubdomain(of: DomainName) -> Bool
public func isStrictSubdomain(of: DomainName) -> Bool
public func isSuperdomain(of: DomainName) -> Bool
public func isStrictSuperdomain(of: DomainName) -> Bool
public func isExactlyEqual(to: Self) -> Bool // also compares isFQDN
public func makeIterator() -> Iterator
}
extension DomainName {
@nonexhaustive public enum DescriptionFormat: Sendable { case ascii, unicode }
public struct DescriptionOptions: OptionSet, Sendable {
public static var includeRootLabelIndicator: Self { get }
}
@nonexhaustive public enum ValidationError: Error { /* … */ }
public struct Iterator: Sendable, IteratorProtocol { public mutating func next() -> Label? }
public struct Label: Sendable, Hashable,
CustomStringConvertible, CustomDebugStringConvertible {
public var description: String { get }
public var debugDescription: String { get }
@available(SwiftStdlib 5.1, *)
public func description(format: DomainName.DescriptionFormat) -> String
}
}
DomainName ⇄ IP address conversions (DomainIPAddressCompat)
Fast, string-free conversions between domain names and IP addresses (dotted-quad and .arpa reverse-DNS forms).
Click for conversion full API
@available(SwiftStdlib 5.1, *)
extension IPv4Address {
public init?(domainName: DomainName) // dotted-quad or .arpa
public init?(arpaDomainName: DomainName)
}
@available(SwiftStdlib 5.1, *)
extension IPv6Address {
public init?(domainName: DomainName) // .arpa
public init?(arpaDomainName: DomainName)
}
@available(SwiftStdlib 5.1, *)
extension AnyIPAddress {
public init?(domainName: DomainName)
public init?(arpaDomainName: DomainName)
}
extension DomainName {
@nonexhaustive public enum IPv4AddressInDomainNameFormatting: Sendable { case dottedQuad, arpa }
public init(ipv4: IPv4Address, format: IPv4AddressInDomainNameFormatting = .arpa)
@available(SwiftStdlib 5.1, *) public init(ipv6: IPv6Address) // .arpa
@available(SwiftStdlib 5.1, *) public init(ip: AnyIPAddress) // .arpa
}
Port
public struct Port: Sendable, Hashable {
public let canonicalValue: UInt16
}
Click for Port full API
extension Port {
public var value: Int { get }
public init(canonicalValue: UInt16)
public init(_: Int)
}
extension Port: ExpressibleByIntegerLiteral { public init(integerLiteral: UInt16) }
extension Port: CustomStringConvertible { public var description: String { get } }
ConnectionTarget
The top-level endpoint: an IP + port, a domain name + port, or a Unix domain socket path.
@available(SwiftStdlib 5.1, *)
public struct ConnectionTarget: Sendable, Hashable, CustomStringConvertible {
public private(set) var target: Target
@nonexhaustive public enum Target: Sendable, Hashable, CustomStringConvertible {
case ipAddress(AnyIPAddress, port: Port)
case domainName(DomainName, port: Port)
case unixDomainSocketAddress(String)
}
}
Click for ConnectionTarget full API
@available(SwiftStdlib 5.1, *)
extension ConnectionTarget {
public static func ipAddress(_: String, port: Port) throws(Error) -> Self
public static func ipAddress(_: AnyIPAddress, port: Port) -> Self
public static func ipAddress(_: IPv4Address, port: Port) -> Self
public static func ipAddress(_: IPv6Address, port: Port) -> Self
public static func domainName(_: String, port: Port,
idnaConfiguration: IDNA.Configuration = .default) throws -> Self
public static func domainName(_: DomainName, port: Port) -> Self
public static func unixDomainSocketAddress(_: String) -> Self
public var description: String { get }
@nonexhaustive public enum Error: Swift.Error, CustomStringConvertible {
case invalidIPAddressString(String)
case failedToParseDomainName(any Swift.Error)
}
}
Performance
This is a copy of the current README performance section:
Click for Performance overview
Performance
- Below are benchmarks of this library against inet C-library APIs of macOS's Darwin and Linux's glibc.
- In all cases, swift-endpoint wins against the inet C APIs.
- These benchmarks are meant to represent a slow-case scenario of real-world workloads.
- The C API benchmarks represent a C language user's experience. Meaning that they don't contain any overhead coming from interfacing with Swift.
- For example converting C character-strings to
Strings for ip-address->string conversions. Specially if the character-string is over 15 bytes of length, which would forceStringto incur a heap allocation. - To give you an idea: in a simple usage of C APIs in Swift, C API results for serialization can be up to 3 times slower due to heap-allocated
String. For parsing the overhead can be marginal, or up to 50% higher. - The swift-endpoint API benchmarks go through those overheads anyway, such as the
String's heap-allocation, but they still manage to beat the C API benchmarks.
- For example converting C character-strings to
- Each benchmark runs against 16 different IPs one by one in a random manner, via a constant seed to keep the benchmarks consistent across benchmark runs.
- This means CPUs won't find a clear pattern to over-optimize for in any of the operations, which would make the benchmarks less realistic.
Against Darwin
These were performed on my M1 Pro MacBook, on macOS 27.
| IP Type | Operation | Swift (ns/op) | inet (ns/op) | Speedup |
|---|---|---|---|---|
| IPv4 | Serializing | 16.1 | 176.0 | 10.93x |
| IPv4 | Parsing | 14.4 | 45.8 | 3.18x |
| IPv6 | Serializing | 84.0 | 237.0 | 2.82x |
| IPv6 | Parsing | 29.3 | 96.5 | 3.29x |
Against glibc
These were performed on a dedicated-cpu-core machine from Hetzner, on Ubuntu 24.04.
| IP Type | Operation | Swift (ns/op) | inet (ns/op) | Speedup |
|---|---|---|---|---|
| IPv4 | Serializing | 20.0 | 100.0 | 5.00x |
| IPv4 | Parsing | 17.0 | 25.0 | 1.47x |
| IPv6 | Serializing | 70.0 | 160.0 | 2.29x |
| IPv6 | Parsing | 32.5 | 40.0 | 1.23x |
Additional Notes
Click for Additional Notes
- To see up to date information about performance of this package, please go to this benchmarks list, and choose the most recent benchmark. You'll see a summary of the benchmark there.
- The results above are all reproducible by simply running
scripts/benchmark.shon a machine of your own. - It's worth noting that swift-endpoint APIs win in pretty much any other situation as well, as visible in the benchmarks.
- For example even if you run a benchmark over only 1 IP so CPUs can over-optimize for the specific IP's case and run it as fast as possible. This might even widen the speed gap and be advantageous to swift-endpoint APIs.
- This is to say the above tables are not an over-representation of this library's capabilities.