Swift struct layout guarantees when using Windows IOCP

I have been playing around with the various networking APIs Windows has to offer and now I have advanced to experimenting with I/O Completion Ports (IOCP). There is a peculiar, I’d say “pointer technique”, that is apparently quite common when dealing with IOCP. However, I’m not sure whether it is totally sound in Swift, due to its lack of guarantees around struct layout etc.

Currently, I’m trying different ways of writing a TCP server. In the IOCP world, you submit I/O requests to an I/O completion port and then wait for any of the requests to finish processing their results. In my case, you first create an ordinary TCP socket, bind it to an address and call listen on it. Then you construct the central I/O completion port and dispatch an accept request. After that, you wait for I/O completions through GetQueuedCompletionStatus. Something along the lines of

import WinSDK
// Create socket
let listenSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP.rawValue)
bind(listenSocket, addr, addrLen)
listen(listenSocket, backlog)

// Create IO completion port
let iocp = CreateIoCompletionPort(INVALID_HANDLE_VALUE, nil, 0, 1)
// Create IO completion port for the listening socket
CreateIoCompletionPort(HANDLE(bitPattern: UInt(listenSocket)), iocp, 0, 0)

// Submit initial accept
let newSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP.rawValue)
let buffer: UnsafeRawBufferPointer = .allocate(capacity: 1024, alignment: 1)
let overlappedPointer: UnsafeMutablePointer<OVERLAPPED> = .allocate(capacity: 1)
overlappedPointer.initialize(to: .init())
// acceptExPtr is a function pointer obtained through a call to WSAIoctl
acceptExPtr(
    listenSocket,
    newSocket,
    buffer,
    0,
    DWORD(MemoryLayout<sockaddr_in>.size + 16),
    DWORD(MemoryLayout<sockaddr_in>.size + 16),
    nil,
    overlappedPointer
)

while true {
    var transferred: DWORD = 0
    var key: ULONG_PTR = 0
    var overlapped: UnsafeMutablePointer<OVERLAPPED>? = nil

    GetQueuedCompletionStatus(iocp, &transferred, &key, &overlapped, INFINITE)
    // Process ...
}

It is commonly through the UnsafeMutablePointer<OVERLAPPED> that you identify and process the various I/O completions (you can also use the key to identify requests). When the call to GetQueuedCompletionStatus completes, it is guaranteed that the UnsafeMutablePointer<OVERLAPPED> is the same that was originally passed.

To carry a more convenient kind of context with each request, you can do the following. In my case, I define the struct

enum OperationType: UInt32 {
    case accept
    case recv
    case send
}

struct Context: ~Copyable {
    var overlapped: OVERLAPPED = OVERLAPPED()
    var wsaBuf: WSABUF = WSABUF()
    let buffer: UnsafeMutableRawBufferPointer = .allocate(byteCount: 1024, alignment: 1)
    var operation: OperationType
    var socket: SOCKET

    init(operation: OperationType, socket: SOCKET) {
        self.operation = operation
        self.socket = socket
    }

    deinit { buffer.deallocate() }
}

The initial accept request is then modified

let newSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP.rawValue)
let acceptContext: UnsafeMutablePointer<Context> = .allocate(capacity: 1)
acceptContext.initialize(to: .init(operation: .accept, socket: newSocket))
let overlappedPointer: UnsafeMutablePointer<OVERLAPPED> = .init(OpaquePointer(acceptContext))
let _ = acceptExPtr(
    listenSocket,
    newSocket,
    acceptContext.pointee.buffer.baseAddress,
    0,
    DWORD(MemoryLayout<sockaddr_in>.size + 16),
    DWORD(MemoryLayout<sockaddr_in>.size + 16),
    nil,
    overlappedPointer
)

while true {
    var transferred: DWORD = 0
    var key: ULONG_PTR = 0
    var overlapped: UnsafeMutablePointer<OVERLAPPED>? = nil

    GetQueuedCompletionStatus(iocp, &transferred, &key, &overlapped, INFINITE)
    let context: UnsafeMutablePointer<Context> = .init(OpaquePointer(overlapped!))
    switch context.pointee.operation {
        case .accept:
            // ...
        case .recv:
            // ...
        case .send:
            // ...
    }
}

and currently it works like a charm.

When I first came across this trick (while looking at C examples), I was initially very confused as to why these casts would be legal in any world

// In C
// OVERLAPPED* overlappedPointer = (OVERLAPPED*)acceptContext;
let acceptContext: UnsafeMutablePointer<Context>
let overlappedPointer: UnsafeMutablePointer<OVERLAPPED> = .init(OpaquePointer(acceptContext))
// In C
// Context* context = (Context*)overlapped;
var overlapped: UnsafeMutablePointer<OVERLAPPED>? = nil
GetQueuedCompletionStatus(iocp, &transferred, &key, &overlapped, INFINITE)
let context: UnsafeMutablePointer<Context> = .init(OpaquePointer(overlapped!))

The reason, at least in C, is that since Context.overlapped is the first member of the struct, the address of that member is exactly the same as the address of the struct itself. My question is whether this is legal in the Swift world. I have read many times that Swift structs do not have a guaranteed layout, but I'm not entirely sure to what extent. In this case it would be useful to be able to assume that the memory address of the first member in a struct would be the same as the struct’s address. Of course one work around would be to define my struct in C and import that (or use the key parameter above). But it is extremely convenient to define a non-copyable struct with all the necessary data for these asynchronous calls, pass it around and clean up everything on deinit.

Are you not able to use pointer(to:) to get a pointer to the property instead?

Thanks, I indeed wasn't aware of that function. Unfortunately, that only solves the (in C)

OVERLAPPED* overlappedPointer = (OVERLAPPED*)acceptContext;

direction. I don't see how I can go the other way around without assuming something about the memory layout of Swift structs.

Oh, if you need to go both directions, I believe KeyPath has a property that returns the offset (as an Optional<Int>, in case it’s a computed property).

@frozen structs have de jure guaranteed layout.

This is the safest option.

Maybe this could help:

struct SwiftStruct: ~Copyable {
    var cstruct: CStruct
    deinit {
        // ...
    }
}

BTW, 1024 seems small enough for stack, YMMV.

2 Likes

I was under the impression that @frozen just guarantees that the layout won't change in future Swift versions, not that the stored properties are in the order they are defined. In the docs they say that

Now, I don't know if this means that the properties can or cannot still be reordered, and that they are only referring to padding etc.

Sure, though if there are plenty of connections then we might run out of stack space. The example given is just a barebones sketch to mainly give the idea where and why this "pointer gymnastic" comes into play.

I mean it can't change, so if it is alright for your purposes today (is it?) it will be alright in the future.

Yes for my purposes it works fine currently. It has just come up in so many places that the layout of Swift structs isn't to be trusted which is why I am so skeptical whether this will break out of nowhere :sweat_smile:

Strictly speaking, the layout is only guaranteed for ABI-stable platforms (i.e. Darwin).

3 Likes

@frozen structs published from currently library-evolution-enabled binaries have the layouts they have, but we will not necessarily maintain the same default layout rules in perpetuity for new code, even on Darwin, even with library evolution enabled.

2 Likes