-
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
FileHelpers: Add FileDescriptor.read(filling buffer:) #84
base: main
Are you sure you want to change the base?
Changes from 3 commits
c7cba01
1740da9
b60b0fa
440fda2
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 |
---|---|---|
|
@@ -76,6 +76,76 @@ extension FileDescriptor { | |
} | ||
} | ||
|
||
/// Reads bytes at the current file offset into a buffer until the buffer is filled. | ||
/// | ||
/// - Parameters: | ||
/// - buffer: The region of memory to read into. | ||
/// - Returns: The number of bytes that were read, equal to `buffer.count`. | ||
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. If it's always equal, then we'd return a Bool. But, if we might only partly fill for an oversized buffer, then it makes sense to return the count. 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. That makes sense. If we go the route of returning a /// Writes a sequence of bytes to the current offset
/// and then updates the offset.
///
/// - Parameter sequence: The bytes to write.
/// - Returns: The number of bytes written, equal to the number of elements in `sequence`.
///
/// This method either writes the entire contents of `sequence`,
/// or throws an error if only part of the content was written.
...
@_alwaysEmitIntoClient
@discardableResult
public func writeAll<S: Sequence>(
... 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. Beyond consistency, returning an integer has the very slight convenience benefit that I can write Returning a boolean instead of throwing an error on EOF is an option to consider. It's sort of consistent with the original interfaces, but it has the same convenience issue as returning a partial buffer -- callers would need to manually check for it and in the partial case they will typically end up throwing anyway. So I'm slightly preferring having this return 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. Current state in PR returns an |
||
/// | ||
/// This method either reads until `buffer` is full, or throws an error if | ||
/// only part of the buffer was filled. | ||
/// | ||
/// The <doc://com.apple.documentation/documentation/swift/unsafemutablerawbufferpointer/3019191-count> property of `buffer` | ||
/// determines the number of bytes that are read into the buffer. | ||
@_alwaysEmitIntoClient | ||
@discardableResult | ||
public func read( | ||
filling buffer: UnsafeMutableRawBufferPointer | ||
) throws -> Int { | ||
return try _read(filling: buffer).get() | ||
} | ||
|
||
/// Reads bytes at the current file offset into a buffer until the buffer is filled or until EOF is reached. | ||
/// | ||
/// - Parameters: | ||
/// - offset: The file offset where reading begins. | ||
/// - buffer: The region of memory to read into. | ||
/// - Returns: The number of bytes that were read, at most `buffer.count`. | ||
/// | ||
/// This method either reads until `buffer` is full, or throws an error if | ||
/// only part of the buffer was filled. | ||
/// | ||
/// The <doc://com.apple.documentation/documentation/swift/unsafemutablerawbufferpointer/3019191-count> property of `buffer` | ||
/// determines the number of bytes that are read into the buffer. | ||
@_alwaysEmitIntoClient | ||
@discardableResult | ||
public func read( | ||
fromAbsoluteOffset offset: Int64, | ||
filling buffer: UnsafeMutableRawBufferPointer | ||
) throws -> Int { | ||
return try _read(fromAbsoluteOffset: offset, filling: buffer).get() | ||
} | ||
|
||
@usableFromInline | ||
internal func _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. If we want to make this function opaque, then it'll need to come with availability, which means that the @milseman Is there a good reason |
||
fromAbsoluteOffset offset: Int64? = nil, | ||
filling buffer: UnsafeMutableRawBufferPointer | ||
) -> Result<Int, Errno> { | ||
var idx = 0 | ||
loop: while idx < buffer.count { | ||
let readResult: Result<Int, Errno> | ||
if let offset = offset { | ||
readResult = _read( | ||
fromAbsoluteOffset: offset + Int64(idx), | ||
into: UnsafeMutableRawBufferPointer(rebasing: buffer[idx...]), | ||
retryOnInterrupt: true | ||
) | ||
} else { | ||
readResult = _readNoThrow( | ||
into: UnsafeMutableRawBufferPointer(rebasing: buffer[idx...]), | ||
retryOnInterrupt: true | ||
) | ||
} | ||
switch readResult { | ||
case .success(let numBytes) where numBytes == 0: break loop // EOF | ||
case .success(let numBytes): idx += numBytes | ||
case .failure(let err): return .failure(err) | ||
} | ||
} | ||
assert(idx <= buffer.count) | ||
return .success(idx) | ||
} | ||
|
||
/// Writes a sequence of bytes to the given offset. | ||
/// | ||
/// - Parameters: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -158,14 +158,23 @@ extension FileDescriptor { | |
into buffer: UnsafeMutableRawBufferPointer, | ||
retryOnInterrupt: Bool = true | ||
) throws -> Int { | ||
try _read(into: buffer, retryOnInterrupt: retryOnInterrupt).get() | ||
try _readNoThrow(into: buffer, retryOnInterrupt: retryOnInterrupt).get() | ||
} | ||
|
||
// NOTE: This function (mistakenly marked as throws) is vestigial but remains to preserve ABI. | ||
@usableFromInline | ||
internal func _read( | ||
into buffer: UnsafeMutableRawBufferPointer, | ||
retryOnInterrupt: Bool | ||
) throws -> Result<Int, Errno> { | ||
_readNoThrow(into: buffer, retryOnInterrupt: retryOnInterrupt) | ||
} | ||
|
||
@usableFromInline | ||
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 needs availability, right @lorentey ? 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. Unfortunately yes, and on ABI stable platforms, Given the importance of this particular entry point, I think it's worth considering marking the new non-throwing variant Otherwise we'll need to mess with I think it's okay to land this without resolving the availability mess -- ABI stable builds aren't possible to do within this repo, and I wouldn't want to force Simon to have to deal with a tricky problem based on partial info and no tooling. It's tricky enough to get right when we have the benefit of the ABI checker and a working build system. (Unless he really really wants to do it, of course!) 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.
😅 sounds like I might be opening a can of worms here. It sounds like merging this PR without addressing the ABI issue might be the quickest solution. After merging though, I'm happy to collaborate on how to please the ABI checker, but I confess this isn't something I've done before. |
||
internal func _readNoThrow( | ||
into buffer: UnsafeMutableRawBufferPointer, | ||
retryOnInterrupt: Bool | ||
) -> Result<Int, Errno> { | ||
valueOrErrno(retryOnInterrupt: retryOnInterrupt) { | ||
system_read(self.rawValue, buffer.baseAddress, buffer.count) | ||
} | ||
|
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.
What happens if the file runs out of bytes?
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.
Well, I guess that's up for grabs. When you added this comment it would block. But this doesn't seem right. I added an update which checks for EOF and returns (which makes returning an
Int
more relevant again).There's another option to consider. We could have
read(filling:)
throw an error if it encounters EOF before the buffer is filled. What do you think?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.
I've used libraries where the fill-this-buffer-by-reading-bytes operation would signal an error if it runs out of bytes and I used ones where it would return a partial buffer. Generally I found the erroring variant more convenient, as in the other case I'd usually just end up having to signal an error myself. (This is supposing that the regular partial
read
is available as a separate operation, like is the case with System.) So from a usability standpoint I'd prefer this threw an error on EOF.Unfortunately, there is no errno value that represents the EOF condition -- the original i/o interfaces do not treat this as an error. So we'll have to define a custom
Error
type if we want to do this.(We'll need to step carefully here, as I'd prefer if we did not start a slippery slope where we end up with a full blown File abstraction within System -- this library ought to simply sanitize system interfaces, not reinvent them.)
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.
I think returning the actual number of bytes read is better than throwing. How do FIFOs, etc. work in this respect?
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.
The current state of this PR is that it will return an
Int
for the number of bytes read, which will be at mostbuffer.count
. If there were enough bytes in the file to fill the buffer then it does fill the buffer and returnsbuffer.count
, if EOF is encountered then it will not throw, but return the number of bytes read into the buffer, which will be less thanbuffer.count
.How do we feel about that? LMK if you want something different.