-
Notifications
You must be signed in to change notification settings - Fork 102
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?
FileHelpers: Add FileDescriptor.read(filling buffer:) #84
Conversation
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.
IIUC this is ABI breaking.
CC @lorentey
Yep, sadly we cannot remove the existing, throwing entry point. We could introduce a new non-throwing one while keeping the original around, but unfortunately it would need to have a more recent availability declaration, and we could only call it after an |
Makes sense, well this was in favour of me adding Maybe I'll repurpose this PR for that since it provides a real motivation for the non-throwing, internal |
We couldn't do I wonder if there's anything similar to |
Do I assume that it's not appealing to have a wrapper that still deals with |
|
Ah, right, I meant one that would read until the UMRBP was filled. Clearly |
@lorentey, thoughts? |
Because `read(into:)` and `write(into:)` return the number of bytes read or written which may be smaller than desired, users will often want to call these functions in a loop until they have read or written all the bytes they require. Such loops require keeping track of the index and will be repeated toil in each application. Swift System already provides an extensions `writeAll(_:)` and `writeAll(toAbsoluteOffset:_:)` which operates on a sequence of bytes and writes all the bytes in the sequence. This patch adds an analogous helper function for reading, `read(filling buffer:)` which takes an `UnsafeMutableRawBufferPointer` and reads until the buffer is full, or until EOF is reached.
a54ce83
to
c7cba01
Compare
@milseman wrote:
OK, well I repurposed this PR for |
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.
@lorentey can you take a look? What do you think the behavior should be?
_readNoThrow(into: buffer, retryOnInterrupt: retryOnInterrupt) | ||
} | ||
|
||
@usableFromInline |
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.
This needs availability, right @lorentey ?
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.
Unfortunately yes, and on ABI stable platforms, read
will need to dispatch to either _read
or _readNoThrow
, depending on the platform & version we're running on.
Given the importance of this particular entry point, I think it's worth considering marking the new non-throwing variant @_alwaysEmitIntoClient @inline(never)
to prevent the availability check.
Otherwise we'll need to mess with #if SWIFT_PACKAGE
to enable/disable availability checking based on whether or not we're building the ABI stable variant.
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 comment
The reason will be displayed to describe this comment to others. Learn more.
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!)
😅 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.
Sources/System/FileHelpers.swift
Outdated
@@ -79,6 +79,75 @@ extension FileDescriptor { | |||
} | |||
} | |||
|
|||
/// Reads bytes at the current file offset into a buffer until the buffer is filled. |
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?
Upon successful completion, read(), readv(), pread(), and preadv() return
the number of bytes actually read and placed in the buffer. The system
guarantees to read the number of bytes requested if the descriptor
references a normal file that has that many bytes left before the end-of-
file, but in no other case.
RETURN VALUES
If successful, the number of bytes actually read is returned. Upon reading
end-of-file, zero is returned. Otherwise, a -1 is returned and the global
variable errno is set to indicate the error.
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 most buffer.count
. If there were enough bytes in the file to fill the buffer then it does fill the buffer and returns buffer.count
, if EOF is encountered then it will not throw, but return the number of bytes read into the buffer, which will be less than buffer.count
.
How do we feel about that? LMK if you want something different.
Sources/System/FileHelpers.swift
Outdated
/// | ||
/// - 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 comment
The 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 comment
The reason will be displayed to describe this comment to others. Learn more.
That makes sense. If we go the route of returning a Bool
are we happy to accept the asymmetry it will create with the writeAll(_:)
helpers, which currently return Int
even though the docs for writeAll(_:)
state:
/// 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 comment
The 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 c += read(filling: buffer)
to keep a running total of bytes read. (read(filling: buffer); c += buffer.count
works but it is more error prone)
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 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.
Current state in PR returns an Int
, discussed in more detail in #84 (comment).
Signed-off-by: Si Beaumont <[email protected]>
@swift-ci please test |
Sources/System/FileHelpers.swift
Outdated
@@ -79,6 +79,75 @@ extension FileDescriptor { | |||
} | |||
} | |||
|
|||
/// Reads bytes at the current file offset into a buffer until the buffer is filled. |
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.)
Sources/System/FileHelpers.swift
Outdated
/// | ||
/// - 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 comment
The 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 c += read(filling: buffer)
to keep a running total of bytes read. (read(filling: buffer); c += buffer.count
works but it is more error prone)
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 buffer.count
.
_readNoThrow(into: buffer, retryOnInterrupt: retryOnInterrupt) | ||
} | ||
|
||
@usableFromInline |
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.
Unfortunately yes, and on ABI stable platforms, read
will need to dispatch to either _read
or _readNoThrow
, depending on the platform & version we're running on.
Given the importance of this particular entry point, I think it's worth considering marking the new non-throwing variant @_alwaysEmitIntoClient @inline(never)
to prevent the availability check.
Otherwise we'll need to mess with #if SWIFT_PACKAGE
to enable/disable availability checking based on whether or not we're building the ABI stable variant.
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!)
} | ||
|
||
@usableFromInline | ||
internal func _read( |
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.
If we want to make this function opaque, then it'll need to come with availability, which means that the read(filling:)
methods will need to have it too.
@milseman Is there a good reason writeAll
is implemented using an opaque function?
I'm fine with the I don't think this package needs to provide an operation that reads the entire file into memory returning a freshly allocated, dynamically sized, owning buffer. (That would be a new operation, independent of |
Signed-off-by: Si Beaumont <[email protected]>
Does that mean the use site needs to first call
What's your use case like? Is the idea that someone gave you a hard-request for exactly some number of bytes? Would you want to write the bytes you could and throw? @simonjbeaumont, what's the immediate intended use of this?
Opaque is the default, and it's not as clear to me the benefits of making it not-opaque just to inline the loop into user code. I'm not sure if it would improve non-blocking I/O, though. |
No! The use site will typically want to read exactly It's not the best use of system resources to use unbuffered I/O like this (i.e., one syscall per primitive value read or written), but given that we provide
If it's okay not to fill up the buffer, then the regular |
I'm asking because these utility functions can be expressed on top of existing public API, and moving them behind a resilience boundary forces them to come with more recent availability on Darwin. These would probably be great use cases for the new |
Yes, but read goes in the opposite direction and we do not have a universal appendable untyped storage object. We could have a readAll that returns a
Yes, for normal files that is guaranteed. I wasn't clear on what happens if it's a fifo. Would it just block and wait for more bytes? |
The equivalent operation to
The read-side equivalent to that is to read exactly
I don't understand this question. What is guaranteed? The exactly-fill-this-fixed-size-buffers-with-bytes-or-throw-an-error operation would call |
Because
read(into:)
andwrite(into:)
return the number of bytes read or written which may be smaller than desired, users will often want to call these functions in a loop until they have read or written all the bytes they require. Such loops require keeping track of the index and will be repeated toil in each application.Swift System already provides an extensions
writeAll(_:)
andwriteAll(toAbsoluteOffset:_:)
which operates on a sequence of bytes and writes all the bytes in the sequence.This patch adds an analogous helper function for reading,
read(filling buffer:)
which takes anUnsafeMutableRawBufferPointer
and reads until the buffer is full, or until EOF is reached.