On difference between process::exit_code and ExitCode

Hi all,

I'm maintaining Hurl, a HTTP cli.

In our CI, we run Valgrind on every commit. In a recent commit, a code modification has triggered a memory leak. We isolate the code that triggers it and it boils down to:

use std::{env, process};
use std::collections::HashMap;

fn main() {
    let _map: HashMap<String, String> = env::vars().collect();
    process::exit(0);
}

With the help of the cargo-valgrind maintainer, we fix it by changing the main function signature from fn main() to fn main() -> ExitCode and avoiding process::exit function. Indeed, in the Rust std documentation, it is stated that

Note that because this function never returns, and that it terminates the process, no
destructors on the current stack or any other thread’s stack will be run. If a clean shutdown
is needed it is recommended to only call this function at a known point where there are no
more destructors left to run; or, preferably, simply return a type implementing Termination (
such as ExitCode or Result) from the main function and avoid this function

What I would like to understand is where could I see the "magic" explaining why/how a main's returning an ExitCode fixes this leak. Basically, what's the difference between fn main() and fn main() -> ExitCode that explains that all resources all well dropped.

Thanks a lot,

Jc

All variables are dropped at the end of the scope, so your code is equivalent to this:

fn main() {
    let _map: HashMap<String, String> = env::vars().collect();
    process::exit(0);
    drop(map);
}

whereas returning ExitCode is equivalent to this:

fn main() -> ExitCode {
    let _map: HashMap<String, String> = env::vars().collect();
    drop(map);
}

fn real_main() {
    let ret = main();
    std::process::exit(ret);
}
6 Likes

:+1:Thanks a lot for your crystal clear explanation. Just wondering (sorry if this is obvious), is the compiler creates a kind of real_main function when the user code main has this signature fn main() -> ExitCode? Are there pointers (doc or source code) where I can find more information on how the main function is managed by the compiler?
Thanks a lot once again!!

Yes, the compiler wraps your main function in its own wrapper. How exactly that is done is target-specific.

Relevant docs:

1 Like

Does this apply to main that returns a Result?

fn main() -> anyhow::Result<> {
  //  ...

  Ok(ExitCode::SUCCESS)

The list of implementors of Termination trait answers your question.

1 Like

I do something like this

 // There is nothing to drop so calling exit() is safe.
        fn main() {
            std::process::exit(my_main()));
        }

and have the my_main function return an integer.

I figured that out before the "ExitCode" and "Termination" trait were stable. This feels simpler.