There's an obstacle right at the beginning here: There is no way to make OpenOptions::open()
return a different type depending on the runtime value of self.
But that's actually how we could get around the backward compatibility problems: Define a new set of File-like types, for concreteness let's call them ReadableFile
, WritableFile
, and UpdatableFile
. Have ReadableFile
implement io::Read
but not io::Write
, and so on. Pair these with a set of new file-opening functions that cover all the sensible ways to open a file and return the appropriate member of the set. I think this is an exhaustive set of combinations of the basic Unix open(2)
flags that make sense:
new_fs::[function] |
returns |
Unix open(2) flags |
read |
ReadableFile |
O_RDONLY |
rewrite |
WritableFile |
O_WRONLY | O_TRUNC |
append |
WritableFile |
O_WRONLY | O_APPEND |
update |
UpdatableFile |
O_RDWR |
create_new |
WritableFile |
O_WRONLY | O_CREAT | O_EXCL |
create_or_rewrite |
WritableFile |
O_WRONLY | O_CREAT | O_TRUNC |
create_or_append |
WritableFile |
O_WRONLY | O_CREAT | O_APPEND |
create_or_update |
UpdatableFile |
O_RDWR | O_CREAT |
Each of these would be shorthand for new_fs::OpenOptions::new().[function](path)
; we'd keep OpenOptions around strictly for the more exotic O_ flags and the mode argument, and their equivalents on non-Unix platforms.
One would also like to expose "is this a seekable file?" in the type system. This is independent of how the file is opened, except that opening a file for appending means it's definitely not seekable. So we would need another three types, SeekableReadableFile
etc. distinguished from the unqualified ones by the fact that they implement io::Seek
. Whether an OS-level file handle is seekable depends on exactly what you opened, and that can't be put into the type system, but whether the program needs the handle to be seekable can be put into the type system. It's just another batch of factory functions.
new_fs::[function] |
returns |
Unix open(2) flags |
read_random_access |
SeekableReadableFile |
O_RDONLY |
rewrite_random_access |
SeekableWritableFile |
O_WRONLY | O_TRUNC |
update_random_access |
SeekableUpdatableFile |
O_RDWR |
create_or_rewrite_random_access |
SeekableWritableFile |
O_WRONLY | O_CREAT | O_TRUNC |
create_or_update_random_access |
SeekableUpdatableFile |
O_RDWR | O_CREAT |
These would have to fail if they discover that the thing you opened is not seekable. Fortunately, lseek(h, 0, SEEK_CUR)
fails when applied to a non-seekable handle h
but has no other side effects, so that's easy to implement.
(The append
functions never give you a seekable, as I mentioned earlier, and create_new
might as well always give you a seekable because the fact that you just created it means it's definitely a "regular file" and therefore seekable. So that does cut down on the combinatorial explosion here a bit.)
This is quite a bit of new complexity, and we'd have to think carefully about all the names and what's left for OpenOptions
to do and the interaction with the rest of std::io
. But I would encourage you to try prototyping it in a crate anyway. Nothing here requires tight integration with the core language, and it could turn out to be worth the hassle of a changeover.
Sure would be nice if there was a way to not need so many new top-level factory functions, huh? Well, maybe there is. We could always go through OpenOptions
, and have it take a type argument that specifies what kind of File we want to get in the end. Let's rename it to just Open
for shorter.
let fh: fs::SeekableReadableFile = fs::Open::new()
.custom_flags(libc::O_DIRECT) // for example
.read(pathname);
The difference between read
and read_random_access
is handled by using (implicitly) an Open<SeekableReadableFile>
instead of an Open<ReadableFile>
.
I'm not sure this is actually better. I might need to try writing a bunch of code that uses both versions of the API to get a proper sense of what works more smoothly.