-
Notifications
You must be signed in to change notification settings - Fork 109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Draft changes to the io_uring prototype #208
base: main
Are you sure you want to change the base?
Changes from 1 commit
15794f5
6338029
996e940
1f821a8
0762f57
d099546
b584504
b596783
6b4084c
baab9b2
6029936
10c070a
32966f9
822e481
fcb0b69
6f793cf
a9f92a6
1a3e37d
f369347
ef94a37
7ea32ae
55fd6e7
e5fdf9e
fdbceca
7107a57
02481e0
bb03f0f
a396967
d4ca412
5bfed03
74366c3
7f6e673
0c6ef16
a22e5f6
6983196
5ba1377
0064649
901f4c8
283e8d6
d895f2a
f3b8cc4
d338de1
5e24673
49dd797
91155fd
9ad16c0
880ec90
48455a9
72c316b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,12 @@ | ||
@_implementationOnly import CSystem | ||
|
||
|
||
public final class AsyncFileDescriptor { | ||
public struct AsyncFileDescriptor: ~Copyable { | ||
@usableFromInline var open: Bool = true | ||
@usableFromInline let fileSlot: IORingFileSlot | ||
@usableFromInline let ring: ManagedIORing | ||
|
||
public static func openat( | ||
atDirectory: FileDescriptor = FileDescriptor(rawValue: -100), | ||
atDirectory: FileDescriptor = FileDescriptor(rawValue: -100), | ||
path: FilePath, | ||
_ mode: FileDescriptor.AccessMode, | ||
options: FileDescriptor.OpenOptions = FileDescriptor.OpenOptions(), | ||
|
@@ -19,35 +18,37 @@ public final class AsyncFileDescriptor { | |
throw IORingError.missingRequiredFeatures | ||
} | ||
let cstr = path.withCString { | ||
return $0 // bad | ||
return $0 // bad | ||
} | ||
let res = await ring.submitAndWait(.openat( | ||
atDirectory: atDirectory, | ||
path: cstr, | ||
mode, | ||
options: options, | ||
permissions: permissions, | ||
intoSlot: fileSlot | ||
)) | ||
let res = await ring.submitAndWait( | ||
.openat( | ||
atDirectory: atDirectory, | ||
path: cstr, | ||
mode, | ||
options: options, | ||
permissions: permissions, | ||
intoSlot: fileSlot.borrow() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a hack, this should also be an actual borrow |
||
)) | ||
if res.result < 0 { | ||
throw Errno(rawValue: -res.result) | ||
} | ||
|
||
return AsyncFileDescriptor( | ||
fileSlot, ring: ring | ||
) | ||
} | ||
|
||
internal init(_ fileSlot: IORingFileSlot, ring: ManagedIORing) { | ||
self.fileSlot = fileSlot | ||
internal init(_ fileSlot: consuming IORingFileSlot, ring: ManagedIORing) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. more "should be a borrow" |
||
self.fileSlot = consume fileSlot | ||
self.ring = ring | ||
} | ||
|
||
@inlinable @inline(__always) @_unsafeInheritExecutor | ||
public func close() async throws { | ||
let res = await ring.submitAndWait(.close( | ||
.registered(self.fileSlot) | ||
)) | ||
public consuming func close() async throws { | ||
let res = await ring.submitAndWait( | ||
.close( | ||
.registered(self.fileSlot) | ||
)) | ||
if res.result < 0 { | ||
throw Errno(rawValue: -res.result) | ||
} | ||
|
@@ -56,67 +57,99 @@ public final class AsyncFileDescriptor { | |
|
||
@inlinable @inline(__always) @_unsafeInheritExecutor | ||
public func read( | ||
into buffer: IORequest.Buffer, | ||
into buffer: inout UnsafeMutableRawBufferPointer, | ||
atAbsoluteOffset offset: UInt64 = UInt64.max | ||
) async throws -> UInt32 { | ||
let res = await ring.submitAndWait(.read( | ||
file: .registered(self.fileSlot), | ||
buffer: buffer, | ||
offset: offset | ||
)) | ||
let file = fileSlot.borrow() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. more hacks |
||
let res = await ring.submitAndWait( | ||
.readUnregistered( | ||
file: .registered(file), | ||
buffer: buffer, | ||
offset: offset | ||
)) | ||
if res.result < 0 { | ||
throw Errno(rawValue: -res.result) | ||
} else { | ||
return UInt32(bitPattern: res.result) | ||
} | ||
} | ||
|
||
deinit { | ||
if (self.open) { | ||
// TODO: close or error? TBD | ||
@inlinable @inline(__always) @_unsafeInheritExecutor | ||
public func read( | ||
into buffer: borrowing IORingBuffer, //TODO: should be inout? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think inout is right here. We want to loan the buffer to the kernel to read into basically… right? |
||
atAbsoluteOffset offset: UInt64 = UInt64.max | ||
) async throws -> UInt32 { | ||
let res = await ring.submitAndWait( | ||
.read( | ||
file: .registered(self.fileSlot.borrow()), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. borrow hacks |
||
buffer: buffer.borrow(), | ||
offset: offset | ||
)) | ||
if res.result < 0 { | ||
throw Errno(rawValue: -res.result) | ||
} else { | ||
return UInt32(bitPattern: res.result) | ||
} | ||
} | ||
|
||
//TODO: temporary workaround until AsyncSequence supports ~Copyable | ||
public consuming func toBytes() -> AsyncFileDescriptorSequence { | ||
AsyncFileDescriptorSequence(self) | ||
} | ||
|
||
//TODO: can we do the linear types thing and error if they don't consume it manually? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we express this? Need to go look over the relevant S-E proposals again |
||
// deinit { | ||
// if self.open { | ||
// TODO: close or error? TBD | ||
// } | ||
// } | ||
} | ||
|
||
extension AsyncFileDescriptor: AsyncSequence { | ||
public class AsyncFileDescriptorSequence: AsyncSequence { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is only here because ~Copyable structs can't conform to AsyncSequence rn |
||
var descriptor: AsyncFileDescriptor? | ||
|
||
public func makeAsyncIterator() -> FileIterator { | ||
return .init(self) | ||
return .init(descriptor.take()!) | ||
} | ||
|
||
internal init(_ descriptor: consuming AsyncFileDescriptor) { | ||
self.descriptor = consume descriptor | ||
} | ||
|
||
public typealias AsyncIterator = FileIterator | ||
public typealias Element = UInt8 | ||
} | ||
|
||
public struct FileIterator: AsyncIteratorProtocol { | ||
//TODO: only a class due to ~Copyable limitations | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here |
||
public class FileIterator: AsyncIteratorProtocol { | ||
@usableFromInline let file: AsyncFileDescriptor | ||
@usableFromInline var buffer: IORingBuffer | ||
@usableFromInline var done: Bool | ||
|
||
@usableFromInline internal var currentByte: UnsafeRawPointer? | ||
@usableFromInline internal var lastByte: UnsafeRawPointer? | ||
|
||
init(_ file: AsyncFileDescriptor) { | ||
self.file = file | ||
init(_ file: consuming AsyncFileDescriptor) { | ||
self.buffer = file.ring.getBuffer()! | ||
self.file = file | ||
self.done = false | ||
} | ||
|
||
@inlinable @inline(__always) | ||
public mutating func nextBuffer() async throws { | ||
let buffer = self.buffer | ||
|
||
let bytesRead = try await file.read(into: .registered(buffer)) | ||
public func nextBuffer() async throws { | ||
let bytesRead = Int(try await file.read(into: buffer)) | ||
if _fastPath(bytesRead != 0) { | ||
let bufPointer = buffer.unsafeBuffer.baseAddress.unsafelyUnwrapped | ||
let unsafeBuffer = buffer.unsafeBuffer | ||
let bufPointer = unsafeBuffer.baseAddress.unsafelyUnwrapped | ||
self.currentByte = UnsafeRawPointer(bufPointer) | ||
self.lastByte = UnsafeRawPointer(bufPointer.advanced(by: Int(bytesRead))) | ||
self.lastByte = UnsafeRawPointer(bufPointer.advanced(by: bytesRead)) | ||
} else { | ||
self.done = true | ||
done = true | ||
} | ||
} | ||
|
||
@inlinable @inline(__always) @_unsafeInheritExecutor | ||
public mutating func next() async throws -> UInt8? { | ||
public func next() async throws -> UInt8? { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For some reason changing this to take an isolation parameter makes it not compile |
||
if _fastPath(currentByte != lastByte) { | ||
// SAFETY: both pointers should be non-nil if they're not equal | ||
let byte = currentByte.unsafelyUnwrapped.load(as: UInt8.self) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,6 @@ | ||
@_implementationOnly import CSystem | ||
|
||
//TODO: should be ~Copyable, but requires UnsafeContinuation add ~Copyable support | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another workaround for missing features |
||
public struct IOCompletion { | ||
let rawValue: io_uring_cqe | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
import struct CSystem.io_uring_sqe | ||
|
||
public enum IORequest: ~Copyable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unclear to me how we want to structure this. An enum is… kinda elegant, but I haven't figured out how to express "this enum's associated values are all borrows scoped to some other thing's lifetime" yet. |
||
case nop // nothing here | ||
case nop // nothing here | ||
case openat( | ||
atDirectory: FileDescriptor, | ||
path: UnsafePointer<CChar>, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
@@ -12,21 +12,26 @@ public enum IORequest: ~Copyable { | |
) | ||
case read( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the registered/unregistered cases used to be combined, I split 'em up to make the borrows work. Unclear that that's what we want long term |
||
file: File, | ||
buffer: Buffer, | ||
buffer: IORingBuffer, | ||
offset: UInt64 = 0 | ||
) | ||
case readUnregistered( | ||
file: File, | ||
buffer: UnsafeMutableRawBufferPointer, | ||
offset: UInt64 = 0 | ||
) | ||
case write( | ||
file: File, | ||
buffer: Buffer, | ||
buffer: IORingBuffer, | ||
offset: UInt64 = 0 | ||
) | ||
case writeUnregistered( | ||
file: File, | ||
buffer: UnsafeMutableRawBufferPointer, | ||
offset: UInt64 = 0 | ||
) | ||
case close(File) | ||
|
||
public enum Buffer: ~Copyable { | ||
case registered(IORingBuffer) | ||
case unregistered(UnsafeMutableRawBufferPointer) | ||
} | ||
|
||
public enum File: ~Copyable { | ||
case registered(IORingFileSlot) | ||
case unregistered(FileDescriptor) | ||
|
@@ -90,30 +95,21 @@ extension IORequest { | |
request.rawValue.file_index = UInt32(fileSlot.index + 1) | ||
} | ||
case .write(let file, let buffer, let offset): | ||
switch consume buffer { | ||
case .registered(let buffer): | ||
request.operation = .writeFixed | ||
return makeRawRequest_readWrite_registered( | ||
file: file, buffer: buffer, offset: offset, request: request) | ||
|
||
case .unregistered(let buffer): | ||
request.operation = .write | ||
return makeRawRequest_readWrite_unregistered( | ||
file: file, buffer: buffer, offset: offset, request: request) | ||
} | ||
request.operation = .writeFixed | ||
return makeRawRequest_readWrite_registered( | ||
file: file, buffer: buffer, offset: offset, request: request) | ||
case .writeUnregistered(let file, let buffer, let offset): | ||
request.operation = .write | ||
return makeRawRequest_readWrite_unregistered( | ||
file: file, buffer: buffer, offset: offset, request: request) | ||
case .read(let file, let buffer, let offset): | ||
|
||
switch consume buffer { | ||
case .registered(let buffer): | ||
request.operation = .readFixed | ||
return makeRawRequest_readWrite_registered( | ||
file: file, buffer: buffer, offset: offset, request: request) | ||
|
||
case .unregistered(let buffer): | ||
request.operation = .read | ||
return makeRawRequest_readWrite_unregistered( | ||
file: file, buffer: buffer, offset: offset, request: request) | ||
} | ||
request.operation = .readFixed | ||
return makeRawRequest_readWrite_registered( | ||
file: file, buffer: buffer, offset: offset, request: request) | ||
case .readUnregistered(let file, let buffer, let offset): | ||
request.operation = .read | ||
return makeRawRequest_readWrite_unregistered( | ||
file: file, buffer: buffer, offset: offset, request: request) | ||
case .close(let file): | ||
request.operation = .close | ||
switch file { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -51,7 +51,7 @@ internal class ResourceManager<T>: @unchecked Sendable { | |
|
||
struct Resources { | ||
let resourceList: UnsafeMutableBufferPointer<T> | ||
var freeList: [Int] | ||
var freeList: [Int] //TODO: bitvector? | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I bet we can be really efficient here if it matters |
||
} | ||
|
||
let mutex: Mutex<Resources> | ||
|
@@ -90,23 +90,33 @@ public struct IOResource<T>: ~Copyable { | |
@usableFromInline let resource: T | ||
@usableFromInline let index: Int | ||
let manager: ResourceManager<T> | ||
let isBorrow: Bool //TODO: this is a workaround for lifetime issues and should be removed | ||
|
||
internal init( | ||
resource: T, | ||
index: Int, | ||
manager: ResourceManager<T> | ||
manager: ResourceManager<T>, | ||
isBorrow: Bool = false | ||
) { | ||
self.resource = resource | ||
self.index = index | ||
self.manager = manager | ||
self.isBorrow = isBorrow | ||
} | ||
|
||
func withResource() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ? |
||
|
||
} | ||
|
||
//TODO: this is a workaround for lifetime issues and should be removed | ||
@usableFromInline func borrow() -> IOResource<T> { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is awful, but it unblocks me for now |
||
IOResource(resource: resource, index: index, manager: manager, isBorrow: true) | ||
} | ||
|
||
deinit { | ||
self.manager.releaseResource(index: self.index) | ||
if !isBorrow { | ||
manager.releaseResource(index: self.index) | ||
} | ||
} | ||
} | ||
|
||
|
@@ -204,8 +214,6 @@ internal func _getSubmissionEntry(ring: inout SQRing, submissionQueueEntries: Un | |
return nil | ||
} | ||
|
||
// XXX: This should be a non-copyable type (?) | ||
// demo only runs on Swift 5.8.1 | ||
public struct IORing: @unchecked Sendable, ~Copyable { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Does this need to be Sendable? I found it surprising that SQRing and CQRing are protected by Mutex. If I was writing a single threaded program, I wouldn't want that overhead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, we decided to change that |
||
let ringFlags: UInt32 | ||
let ringDescriptor: Int32 | ||
|
@@ -455,9 +463,9 @@ public struct IORing: @unchecked Sendable, ~Copyable { | |
|
||
@inlinable @inline(__always) | ||
public mutating func writeRequest(_ request: __owned IORequest) -> Bool { | ||
let raw = request.makeRawRequest() | ||
var raw: RawIORequest? = request.makeRawRequest() | ||
return submissionMutex.withLock { ring in | ||
return _writeRequest(raw, ring: &ring, submissionQueueEntries: submissionQueueEntries) | ||
return _writeRequest(raw.take()!, ring: &ring, submissionQueueEntries: submissionQueueEntries) | ||
} | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ideally this should be a borrow of the file slot, but that's hard to express right now