The RFC for ?
in main
has gotten hung up on design issues related to process::ExitStatus
that I think need to be pulled out to their own discussion.
For context, process::ExitStatus
is currently only used (in the stdlib) to report a subprocess’s exit status in a Rust parent process. The ?
-in-main
RFC tries to reuse it as a type you can return from main
if you want to report failure to your parent without calling process::exit
or printing any error messages. (I did it this way because I think we shouldn’t allow people to return a bare i32
from main
. I think that will make certain types of beginner mistakes, like getting confused about when you should have a ;
on the last expression in your function, harder to catch.)
The problem with this idea is that ExitStatus
as-is has no constructors intended for public consumption, because right now the only code that has any reason to create ExitStatus
values is the guts of process::Child::wait
. Designing those constructors is where we run into a problem, because boy howdy are there a lot of portability gotchas relating to process exit statuses:
-
The C standard says that
main
returns anint
but only defines the effect of three possible return values: 0,EXIT_SUCCESS
, andEXIT_FAILURE
. The first two report “successful termination” to “the host environment”, and the third reports “unsuccessful termination”. All other values have an implementation-defined effect. (The same behavior is defined for theexit
library function.) -
Unix divides exit statuses into two classes: “exited”, which means that the subprocess called the
_exit
primitive, and “signaled”, which means that the subprocess received a fatal signal. This is a hard distinction; it is not possible to generate a “signaled” exit status by passing any value to_exit
. (Technically there are two other classes of exit status, but only debuggers and shells need to care about them.) -
It is supposed to be possible (according to POSIX.1-2008) to pass an arbitrary
i32
quantity through_exit
towaitid
, but none of the Unixes I can conveniently test (Linux, OSX, FreeBSD, and NetBSD) implements this. They either don’t havewaitid
at all, or only the low 8 or 24 bits of the exit status survive. The much more commonly usedwaitpid
interface can only report the low 8 bits of the exit status. -
The Bourne shell further confuses the issue by mapping “signal” statuses onto 128 + signal number (i.e. if you observe the value 139 in
$?
, that could mean either exit code 139 or signal 11; there’s no way to tell). For this reason, good practice on Unix is to avoid using exit statuses above 127. -
On Windows, unusually, things are simpler. Any DWORD quantity (except 259, which is reserved to mean “that process is still running”) will pass unmolested through
ExitProcess
toGetExitCodeProcess
. There is no such thing as a signal exit status; the catastrophic failure conditions that produce signal exit statuses on Unix instead causeGetExitCodeProcess
to return an appropriate NTSTATUS code (e.g.0xC000_0005
(STATUS_ACCESS_VIOLATION
) is more-or-less equivalent to a reported SIGSEGV on UNIX). The only gotchas are that DWORD isu32
(whereasprocess::exit
takes ani32
) and that there are hundreds of NTSTATUS codes, they’re scattered all over the number space, and (as far as I know) it’s not documented which ones the system might generate in response to a catastrophic failure condition.
Now, in the ?
-in-main
RFC we want to make it easy for programs to signal generic success and failure by returning ExitStatus
values from main
, and that can be handled with zero-argument ExitStatus
constructors corresponding to C’s EXIT_SUCCESS
and EXIT_FAILURE
. There is a minor naming problem because ExitStatus::success()
is already taken for the “is this a successful exit status?” predicate, but that’s not the important design problem I want to talk about.
The important design problem is that we also want a constructor that takes an arbitrary i32 and guarantees to pass that along to process::exit
, and the existing ExitStatusExt::from_raw
(which is the only documented constructor for ExitStatus
at the moment) does not do that job. Well, on Windows it does, because the internal representation for ExitStatus
on Windows is just the DWORD returned by GetExitCodeProcess
, which (except for the value 259) is the same as "a value you can pass to ExitProcess
". But on Unix, the internal representation of an ExitStatus
is the status reported by waitpid
, and that can encode both “exited” and “signaled” statuses, but only “exited” statuses can be converted into a value that can be supplied to _exit
, and let s = ExitStatusExt::from_raw(2)
produces a signaled status on most Unixes. (The exact encoding of a waitpid
status is unspecified in POSIX, but the convention used by Linux and the BSDs both is that an “exited” status has the low byte all-bits-zero and the exit code in bits 8 through 15.)
Given all of that, if backward compatibility were not a concern, I think ExitStatus
ought to look like this:
pub enum ExitStatus {
Exited(i32),
Signaled(i32)
}
impl ExitStatus {
fn success() -> Self { Exited(libc::EXIT_SUCCESS) }
fn failure() -> Self { Exited(libc::EXIT_FAILURE) }
fn from_code(code: i32) -> Self { Exited(code) }
fn from_signal(sig: i32) -> Self { Signaled(sig) }
fn is_successful(&self) -> bool {
if let Exited(n) = *self { n == 0 }
else { false }
}
// can produce either Exited() or Signaled(); OS-specific implementation
fn from_wait_status(status: i32) -> Self;
}
The code
and signal
methods have been removed; if you want more detail than is_successful
, you match on Exited versus Signaled, which is better ergonomics than calling code
and then signal
and having to know that it’s impossible for both of them to return None for the same input. Signaled statuses will never arise from subprocesses on Windows, but portable code needs to consider both anyway.
One way to make this backward compatible would be to make the enum
an internal data carrier, with no methods, but still exposed, and give some things different names:
pub enum DecodedExitStatus { Exited(i32), Signaled(i32) }
pub struct ExitStatus { /* inner: DecodedExitStatus */ }
impl ExitStatus {
// strawman names using the Result convention
fn ok() -> Self { ExitStatus { Exited(libc::EXIT_SUCCESS) } }
fn err() -> Self { ExitStatus { Exited(libc::EXIT_FAILURE) } }
fn from_code(code: i32) -> Self { ExitStatus { Exited(code) } }
fn from_signal(sig: i32) -> Self { ExitStatus { Signaled(sig) } }
fn success(&self) -> bool {
if let Exited(n) = self.inner { n == 0 }
else { false }
}
fn decode(&self) -> DecodedExitStatus { self.inner }
fn code(&self) -> Option<i32> {
match self.inner { Exited(n) => Some(n), _ => None }
}
fn signal(&self) -> Option<i32> {
match self.inner { Signaled(n) => Some(n), _ => None }
}
// can produce either Exited() or Signaled(); OS-specific implementation
fn from_raw(status: i32) -> Self;
}
That’s not perfect but it seems acceptable to me. What do y’all think?
None of this addresses the question of what to do if a “signaled” ExitStatus
is returned from main
, but there are some plausible options (reraise the signal, for instance). If we try to define a type that can only represent “exited” statuses we immediately run into another argument, over whether that should be able to represent any i32 or just the ones that will pass unmolested through the APIs on the current platform, and since we can’t represent that restriction in the type system, does from_code
now return an Option, and what is application code expected to do if it gets None… Better to avoid, I think.