-
Notifications
You must be signed in to change notification settings - Fork 12.8k
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
Should getting a BorrowedHandle
from Stdin
, Stdout
or Stderr
return a Result
on Windows?
#90964
Comments
technically you can also get an error on unix, e.g. the stdio file descriptor is closed. |
Right but that's a different sort of error. If the stdin file descriptor is closed then the stdin file descriptor itself still exists. I mean fd On Windows the value isn't static. It's retrieved at runtime, which can fail. |
I wouldn't quite use the word fail. Internally in Windows, there's a struct with one value for each of the three standard handles. Notably So realistically, the only case worth considering is when the process was launched without stdio handles (e.g. a GUI process). Do you want to return an explicit error in that case, or is a |
Sure I do think that |
I'd argue that returning a closed fd is the equivalent of returning |
Hmm. I think passing |
As do I. But even if we change it now we still have to live with older Rust programs that don't. And in any case I doubt that Rust standard library is the only code that does this. |
But maybe to be defensive it would be wise to normalize both |
Yeah. That's what the standard library does internally. It makes them both an |
I'm confused about how the I just tried an experiment. It seems that in I've so far not been able to find any place where accessing |
It's hard to judge because you seem to be running programs through cargo? In the case of using stdio types themselves, I wouldn't expect them to fail. If the std handle is NULL or INVALID_HANDLE_VALUE it silently pretends any operations are successful without actually calling into the Windows API. See rust/library/std/src/io/stdio.rs Lines 189 to 194 in 68b554e
Code using raw handles do not gain this benefit unless they do similar checks themselves. |
I've now added a patch to run the child processes directly. The child processes still get NULL handles. So while there does exist a function inside |
You code doesn't seem to be checking the exit statuses? In any case I'm uncertain what you're trying to show. I agree that the standard library's internal handling of stdio is great (quibbles about setting null vs invalid aside). Indeed that's my point. It uses proper Rust types and doesn't use sentinel values internally if it can help it. When a foreign interface returns value which may have sentinels, Rust converts near to the boundary. However, this information is lost with the |
Ah, you're right, I was forgetting to check the exit statuses. With that fixed I now indeed see that child processes of a parent that lacks a console get a |
If it's important for users to be aware that a Windows program with no console attached has no I wonder if it's feasible to change |
I would definitely be in favour of this.
Sure. If we were redesigning the high level stdio API that is something I'd be likely to argue for. But I don't think that's on the table.
And to be fair that's perfectly fine for a high level API. It's free to hide the internal implementation details as much as it wants so long as it presents a consistent interface. The trouble is that people want to get at the internals and when they do they carry the high level (or at least POSIX) model with them. Which would be fine if people had to use the Windows API to get it. But Rust itself provides a public lower level API which keeps up the appearance of the POSIX model instead of more correctly modelling the underlying I think this is a particular problem for stronger types like |
What if we were to say that NULL, at this level of abstraction, is a normal valid handle, and not a sentry. It's just a valid handle to a device that just happens to fail on any I/O operation. It does happen to alias a sentry value used in some Windows APIs, but that's similar to how the process handle aliases the If the goal is to give users to have a correct mental model of how Windows works, especially at the lower levels, this seems to be the best way. A NULL handle is literally how the Windows APIs work. Consider the code quoted above: [...]
} else if handle.is_null() {
Err(io::Error::from_raw_os_error(c::ERROR_INVALID_HANDLE as i32))
} else {
[...] This appears to be the reason that Rust has the behavior where child processes of parents with detached consoles see This suggests that for the most accurate mental model of how Windows works, to help users avoid a pattern known to have caused a bug, we should present NULL as a valid handle, and not an error. |
Ok, I've now done some experiments with this. |
I'm not sure I agree with your reasoning.
And indeed In the standard library, stdio is the special case here. Other types, such as Example#![feature(io_safety)]
use std::os::windows::io::{AsHandle, BorrowedHandle, RawHandle};
// A very simplified version of the stdlib type.
struct Stdout;
impl AsHandle for Stdout {
/// Panics if there is no handle avaliable.
/// It's recommended that `try_as_handle` is used in case no stdout handle is avaliable.
fn as_handle(&self) -> BorrowedHandle<'_> {
self.try_as_handle().expect("no stdout handle")
}
}
trait TryAsHandle {
// Alternatively this could return a `Result`.
fn try_as_handle(&self) -> Option<BorrowedHandle<'_>>;
}
impl TryAsHandle for Stdout {
/// Returns the handle if one is avaliable, otherwise returns `None`.
fn try_as_handle(&self) -> Option<BorrowedHandle<'_>> {
unsafe {
let result = GetStdHandle(STD_OUTPUT_HANDLE);
if result.is_null() || result == INVALID_HANDLE_VALUE {
None
} else {
Some(BorrowedHandle::borrow_raw_handle(result))
}
}
}
}
fn main() {
let stdout = Stdout;
// Getting the handle should succeed for a console application...
dbg!(stdout.as_handle());
// Unset the stdout handle.
unsafe {
SetStdHandle(STD_OUTPUT_HANDLE, 0 as _);
}
// Prints `None`.
dbg!(stdout.try_as_handle());
// Panics.
dbg!(stdout.as_handle());
}
// Windows API definitions.
type DWORD = u32;
type BOOL = i32;
const STD_OUTPUT_HANDLE: DWORD = -11_i32 as u32;
const INVALID_HANDLE_VALUE: RawHandle = -1_isize as _;
#[link(name="kernel32")]
extern "system" {
fn GetStdHandle(nStdHandle: DWORD) -> RawHandle;
fn SetStdHandle(nStdHandle: DWORD, hHandle: RawHandle) -> BOOL;
} |
My experiment above to find a way to reconcile stdio handle values in the child with the parent, and my idea above to treat NULL like a valid always-failing handle, were me trying to fit this proposal into a broader scheme. Unfortunately, neither of my idea here turned out to be practical. Without a broader scheme, and with my own current limited understanding of Windows, and the lack of reports of this problem affecting any users of std, I myself am not confident that this proposal will be a net positive. |
I've followed up on this question internally at Microsoft, and we would prefer to have a |
@yoshuawuyts Thanks for following up! I think we generally agree that some form of I'd like to propose #93263 as a way to address this issue. It works by making std's API consistent about always presenting absent stdio handles as null, even in child processes of parents with detached consoles. Compared to the Reasons for And it retains the Reasons not to do |
This addresses rust-lang#90964 by making the std API consistent about presenting absent stdio handles on Windows as NULL handles. Stdio handles may be absent due to `#![windows_subsystem = "windows"]`, due to the console being detached, or due to a child process having been launched from a parent where stdio handles are absent. Specifically, this fixes the case of child processes of parents with absent stdio, which previously ended up with `stdin().as_raw_handle()` returning `INVALID_HANDLE_VALUE`, which was surprising, and which overlapped with an unrelated valid handle value. With this patch, `stdin().as_raw_handle()` now returns null in these situation, which is consistent with what it does in the parent process. And, document this in the "Windows Portability Considerations" sections of the relevant documentation.
…onsole-handle, r=dtolnay Consistently present absent stdio handles on Windows as NULL handles. This addresses rust-lang#90964 by making the std API consistent about presenting absent stdio handles on Windows as NULL handles. Stdio handles may be absent due to `#![windows_subsystem = "windows"]`, due to the console being detached, or due to a child process having been launched from a parent where stdio handles are absent. Specifically, this fixes the case of child processes of parents with absent stdio, which previously ended up with `stdin().as_raw_handle()` returning `INVALID_HANDLE_VALUE`, which was surprising, and which overlapped with an unrelated valid handle value. With this patch, `stdin().as_raw_handle()` now returns null in these situation, which is consistent with what it does in the parent process. And, document this in the "Windows Portability Considerations" sections of the relevant documentation.
…onsole-handle, r=dtolnay Consistently present absent stdio handles on Windows as NULL handles. This addresses rust-lang#90964 by making the std API consistent about presenting absent stdio handles on Windows as NULL handles. Stdio handles may be absent due to `#![windows_subsystem = "windows"]`, due to the console being detached, or due to a child process having been launched from a parent where stdio handles are absent. Specifically, this fixes the case of child processes of parents with absent stdio, which previously ended up with `stdin().as_raw_handle()` returning `INVALID_HANDLE_VALUE`, which was surprising, and which overlapped with an unrelated valid handle value. With this patch, `stdin().as_raw_handle()` now returns null in these situation, which is consistent with what it does in the parent process. And, document this in the "Windows Portability Considerations" sections of the relevant documentation.
#93263 has now been merged. |
On Windows, the standard library's function for getting a stdio handle looks like this:
Note that it only returns a handle if one is set, otherwise it returns an error.
In contrast, the public
AsRawHandle
stdio implementation ignores errors returned byGetStdHandle
and just uses the returned value, whatever that may be.The Safe I/O RFC introduced new types for managing handles. The
AsHandle
trait is intended to be a drop in replacement for the oldAsRawHandle
trait but returns aBorrowedHandle
instead of aRawHandle
.I personally don't think
AsHandle
should be implemented for stdio types. Instead a function with a signature similar to this should be implemented:It would work similarly to the internal
get_handle
function in that it will return an error if there is no handle to return.Reasons for a
try_as_handle()
function instead of implementingAsHandle
on stdio types:BorrowedHandle
should be what the name implies: a borrow of a handle. It should not be an error sentinel value (which may overlap with an actual handle value).Reasons not to do this:
INVALID_HANDLE_VALUE
will probably lead to an error in any case so it's unclear how much of an issue this is in practice.AsRawHandle
andAsHandle
aren't implemented for all the same types.See also: I/O Safety Tracking Issue (#87074) and a previous discussion on internals.
The text was updated successfully, but these errors were encountered: