Most functions could be inferred, but the ultimate source of basically all of these write only APIs is FFI functions, which in turn call systemcalls.
You're at least going to need a way to annotate the FFI calls and systemcalls to describe to the compiler how they access data.
If you're doing some sort of zero-copy IO, the time to clear the buffer might be non-trivial (not huge, but non-trivial). It's true that you need a large enough buffer that syscall/ffi overhead doesn't dominate, but that's not unrealistic.
It's rare that we care about this, that's true, that's why generally rust has been fine with "just zero buffers". There are definitely domains that care though.
If you can think of a different approach of how the compiler can figure out automatically what memory has become initialized by a random function call I’m all ears.
The compiler doesn’t reasonably have any of that information to understand what read is filling in at runtime because that information is encoded purely at runtime and the compiler has no reasoning mechanism even close to answering runtime data flow questions.
There’s also all sorts of complexity that has to do with the kinds of transformations that are possible as the legal information that exists at the language level is often erased before it gets to the stack/register piece and vice versa the language layer knows nothing about registers and minimal stuff about stack.
This is the same reason that the compiler fails to compile something like:
for _ in 1..10 {
let x: String = create_new_string();
eprintln(“{x}”);
}
Fails to hoist x out of the loop even if the returned string is String::new(“ABC”) unless maybe LTO is on (and even then maybe not). Basically the compiler’s “magic” is very limited to static transformations that follow as-if - the compiler must know the static transformation is blindly identical and the amount of reasoning about the structure is often very limited.Said another way, if the compiler could do the optimizations you’re hypothesizing, it would be equivalent to applying a mid level performance engineer to every code base it encounters.
I think it's theoretically possible, but at the cost of much longer compile times, and greater complexity in the compiler.
fn read<T, N: size_t>(&mut self, buf: &mut [MaybeUninit<T>] becomes &[T; N] after call) -> N {
… enforces the body initializes N elements out of buf
}
and then rules that &mut [T] can also be supplied to such functions that today could only accept a &mut [MaybeUninit<T>] transparently.A more likely interface you could write today would look like:
fn read_uninit<T>(&mut self, buf: &mut [MaybeUninit<T>]) -> (&[T], &[MaybeUninit<T>]) {
… enforces the body initializes N elements out of buf
}
You still have to cast &[T] into &[MaybeUninit<T>] somehow.Problem would be: how do you express “you can only access the buffer you sent me through the read-only slice I returned, but you have to free that same buffer when you’re done calling me?
I think that can be done using a function creating a read buffer for a given input stream that
- during calls to read is ‘owned for writing’ by that stream (so, it has to borrow a capability that the creator of the buffer doesn’t have. I don’t think Rust currently supports that)
- where stream.read returns a read only slice whose lifetime is bound to that of the buffer
So, the creator of the buffer can only pass it to read to get a slice back that contains precisely the data read.
The stream can write to the entire buffer.
unsafe{ std::mem::transmute(slice) }
This is probably the only way that will ever exist, because let slice: &mut [NonZeroU8] = ...;
let slice_uninit: &mut [MaybeUninit<NonZeroU8>] = ...;
let nonzero_uninit: &mut MaybeUninit<NonZeroU8> = &mut slice_uninit[0];
*nonzero_uninit = MaybeUninit::zeroed();
slice[0]; // Undefined behavior for sure by now.
Is all safe except for the cast.I.e. MaybeUninit<T> allows you to write invalid bit-patterns to T, so you can't safely cast a reference to T to it (and if you do unsafely cast a reference to T to it you can't soundly write an invalid bit pattern). All current forms of safely making a MaybeUninit take ownership of the value they are declaring to be MaybeUninit for this reason.
I guess at some point we might get methods for this on types that can take on all bit patterns - if/when that's encoded as a trait.