Small updates about swift-endpoint.
- The library now stores big-endian bytes in the underlying storage of
IPv[4/6]Addresstypes.- The storages are still just integer types, but we act like they are each a contiguous sequence of bytes in big-endian.
- There was pretty much no performance impact after adapting the code. Perhaps a minor improvement even.
- Since the underlying value is now in big-endian, and thanks to some underscored attributes, we now have
var bytes: Span<UInt8>accessors for theIPv[4/6]Addresstypes.- Required a few experimental features enabled:
Lifetimesfor declaring lifetime dependency of theSpan<UInt8>.AddressableTypesfor the@_addressableForDependenciesattribute which is assigned to each IP type.BuiltinModulesince we need to useBuiltin.addressOfBorrow(andBuiltin.unprotectedAddressOfBorrow).- The code is similar to
InlineArray.span.
- Required a few experimental features enabled:
These are some of the new IPv[4/6]Address APIs:
bytes: Span<UInt8>_unprotectedBytes: Span<UInt8>asUInt[32/128]()(little-endian)asUInt[32/128](byteOrder: .native/.bigEndian/.littleEndian)
As I planned and mentioned in the meetings, the underlying storage of the IP types are no longer publicly accessible. However you can still practically just read the raw storage via asUInt[32/128](byteOrder: .bigEndian).
The code:
import func Builtin.addressOfBorrow
import func Builtin.unprotectedAddressOfBorrow
@_addressableForDependencies
public struct IPv4Address: Sendable, Hashable {
public static var size: Int { 4 }
/// The underlying 4 bytes representing this IPv4 address, in big-endian byte order.
var _storage: UInt32
public init(_ address: UInt32, byteOrder: ByteOrder = .native) {
self._storage = byteOrder == .bigEndian ? address : address.byteSwapped
}
}
extension IPv4Address {
/// The underlying 32 bits (4 bytes) representing this IPv4 address, as a `UInt32`.
/// For example `IPv4Address("127.0.0.1")!.asUInt32()` is `0x7F00_0001`.
/// Or `IPv4Address("127.0.0.1")!.asUInt32(byteOrder: .bigEndian)` is `0x0100_007F`.
public func asUInt32(byteOrder: ByteOrder = .native) -> UInt32 {
byteOrder == .bigEndian ? self._storage : self._storage.byteSwapped
}
/// A view of exactly 4 (IPv4Address.size) bytes representing this IPv4 address, in network byte order.
/// For example `IPv4Address("127.0.0.1")!.bytes` contains `[127, 0, 0, 1]`.
public var bytes: Span<UInt8> {
@_lifetime(borrow self)
get {
let pointer = UnsafeRawPointer(Builtin.addressOfBorrow(self))
let span = unsafe Span<UInt8>(_unsafeStart: pointer, byteCount: Self.size)
return unsafe _overrideLifetime(span, borrowing: self)
}
}
/// A stack-unprotected view of exactly 4 (IPv4Address.size) bytes representing this IPv4 address, in network byte order.
/// For example `IPv4Address("127.0.0.1")!._unprotectedBytes` contains `[127, 0, 0, 1]`.
///
/// This unprotected accessor means there is no stack canary to protect the stack frame where this
/// accessor will land. An out-of-bounds write from any other code (e.g. when using unsafe-pointers)
/// in that same function will go undetected instead of trapping on the stack canary.
/// Therefore prefer `bytes` unless you trust the calling frame.
public var _unprotectedBytes: Span<UInt8> {
@_lifetime(borrow self)
get {
let pointer = UnsafeRawPointer(Builtin.unprotectedAddressOfBorrow(self))
let span = unsafe Span<UInt8>(_unsafeStart: pointer, byteCount: Self.size)
return unsafe _overrideLifetime(span, borrowing: self)
}
}
}
What is IPv[4/6]Address._unprotectedBytes for?
- This is similar to
IPv[4/6]Address.byteswith only difference being usage ofunprotectedAddressOfBorrowinstead ofaddressOfBorrow. - This is to ensure the compiler doesn't emit a stack canary to possibly stop memory corruptions.
- This can be useful in certain contexts for example when interoperating with C, but also costs 15+ instructions when all you would otherwise need might be like 2.
- It'll insert a "stack canary", meaning 8 bytes, after the address of the bytes of the ip address. If those addresses are modified at the end of the stack frame, then something must have gone wrong, and it'll trap on that.
- To be clear this is not a rare behavior related to
Spans or such. e.g.withUnsafeBytes(of:)also comes with this stack canary.
The compiler appears to be weak around eliminating the stack canary when it's not actually needed at all, so I've put up this PR to hopefully fix some of those:
What is @_addressableForDependencies?
This is an underscored attribute that is not even documented in swiftlang/swift/docs/ReferenceGuides/UnderscoredAttributes.md.
Not until this PR is merged at least: Add `@_addressableForDependencies` to "Underscored Attributes Reference" by MahdiBM · Pull Request #92211 · swiftlang/swift · GitHub
- It ensures the ip-value is addressable in the stack-frame of the caller of
bytes, by passing it tobytesas an address instead of as an address-less value in a register, sobytescan point at that storage that is guaranteed to outlivebytesgetter's stack-frame. - This effect requires both
@_lifetime(borrow self)onbytesand@_addressableForDependencieson the type. - This way the
Span's underlying pointer is guaranteed to remain valid for the caller to read through.