Consider this:
typealias Readable = (UnsafeMutableRawBufferPointer) async throws -> Int
func reader(read: Readable) async throws {
let bufferedBeforeUpgrade = try await parseHttp(read) { read in
while let req = try await read() {
print(req.path)
}
return UnsafeRawBufferPointer(start: nil, count: 0) // not a websocket upgrade
}
if bufferedBeforeUpgrade.baseAddress != nil {
try await parseWebsocket(bufferedBeforeUpgrade, read) { read in
// ...
}
}
}
read()
is the async
version of the read
syscall bound to a berkeley socket. The runtime runs reader()
in its own Task
and will half close the socket once it returns or throws. In this case it would also cancel its sibling doing the writing, but the other one allows read-only half-close.
While the kernel doesn't mind too much, I explicitly want read
to not be sendable because passing it across concurrency domains will cause interleaved reads which are just shy of UB and besides, the goal is to naturally propagate backpressure into the kernel in a composable way, without buffering in userspace. Eventually some synchronization will be required here (between the reader and the writer, or between different sockets) in some but not all cases.
Because a half/full close is issued once the reader returns, read
must never escape (the optimization benefits are extra to this goal). To my understanding, there's currently no way to forward this restriction to a specialized handler (for example http, possibly followed by websocket) that exposes a different interface while still propagating backpressure for its lifetime other than using two non-escaping closures, one of which calls into the other.
While it would match this use case (since the closures are defined in different capture contexts), I am against Joe's proposal to add another annotation by argument of complexity alone. There are already way too many annotations in the language to the point that this is starting to feel like a failure to eat your own dog food: Write swift in C++ so you don't have to be bothered by the restrictions imposed onto devs, and every time someone comes up with a valid reason to break the rules, bless it with a new @_withForbulating
annotation.
Remaining symbols if you want to play with this:
struct HttpRequest { let path: String /* ... */ }
typealias HttpReadable = () async throws -> HttpRequest?
func parseHttp<T>(_ read: Readable, _ cb: (HttpReadable) async throws -> T) async throws -> T {
// unworkable: can't use `read` here
try await cb( { HttpRequest(path: "/") } )
}
func parseWebsocket<T>(_ buffered: UnsafeRawBufferPointer, _ read: Readable, _ cb: (HttpReadable) async throws -> T) async throws -> T {
// this would have a different signature, it's just a copy/paste to keep the example light
try await cb( { HttpRequest(path: "/") } )
}