Exit Codes From Main

I believe @blangmuir and @Joe_Groff have already explained what I suggested:)

As I understand it, standard C and C++ require one of:

int main();

int main(int argc, char *argv[]);

But they don't need a return 0; statement to indicate success, only a nonzero value to indicate failure. This might be better expressed in Swift as () throws -> Void.

Throwing an error also seems preferable to calling a nonreturning exit(_:), because the former might allow defer statements and other cleanup code to run?

I like Joe's idea of defaulted Error requirements.

  • Should the default processExitCode be EXIT_FAILURE?
  • Should presentTopLevelError() have a stderr parameter?
2 Likes

You could presumably just use exit from your local C library here, though, if you were writing something that were that low-dependency.

Personally, I have an aversion to main as a function: it is a lot of extra stuff to learn up front when you are just starting out. Then I have a (mild) aversion to having two equivalent ways to do the same thing if the newer one adds no benefit over the older one.

The big benefit of @main for me was that the main function could come from a library. The benefit of allowing throws is that people didn’t have to learn about exit statuses to handle the 90% case of “my program has failed, it doesn’t matter how”, for either form of the main entry point. Adding a return type to @main doesn’t apply uniformly and doesn’t have the same effect of easing comprehension; learning about “returning from main” isn’t really any simpler than learning about exit (however it ends up being spelled). And because Swift wouldn’t use a name like EXIT_FAILURE, the 90% case of “my program just needs to fail” would either become more verbose (return ExitStatus.failure?) or more baroque (return 1). At least exit(1) or CommandLine.exit(1) becomes a good place to hang documentation.

9 Likes

Maybe we could consider a ProcessExitCodeConvertible protocol, with default implementations that provide sensible mappings on various platforms:

protocol ProcessExitCodeConvertible {
  associatedtype ExitCode // because it’s `int` on POSIX and `UINT` on Windows
  var processExitCode: ExitCode { get }
}

extension Error where Self: ProcessExitCodeConvertible {
  var processExitCode: ExitCode {
    #if WINDOWS
    return UInt(EXIT_FAILURE)
    #else
    return Int(EXIT_FAILURE)
    #endif
  }
}

This still supports throw from top-level code, but requires the developer to think about what that actually means to the system. Throwing @main would of course be enhanced to understand ProcessExitCodeConvertible.

It's not just morally equivalent, it's literally equivalent (unless your C implementation is non-conforming).

If we're going to wrap one, the C standard exit seems like the most common ground we can go for. One benefit of favoring exit functions, though, is that we don't need to expose every platform-specific exit path through a common interface—instead of using the standard exit, code can also reach directly for ExitProcess or _exit or _Exit or quick_exit or task_terminate or ZwTerminateProcess or whatever instead if it needs to.

4 Likes

I’d strongly prefer something like main() -> Error?, if there’s any way to make that work. Or maybe main() -> Result<Whatever, Error> for CLI apps which essentially just wrap a library and would normally print their answer right before exiting anyway.

Except, by returning from main, we do ensure that we get the correct one as the runtime itself will know which one should be associated with the current entry point.

1 Like

I would vastly prefer a return value from main than an exit invocation, purely due to composability. As a concrete example, I currently wrap the Swift Lambda runtime’s main with my own to set up some additional error handling while still delegating to the existing behavior, and if that wrapped method were to explicitly call exit instead of returning or throwing, I would lose the ability to do any final cleanup after that main returns.

Keeping function call semantics (whether via return value or throwing an error) and allowing the highest level to translate that into the appropriate system call feels like a very similar stance to the one around throw vs. fatalError - sure, if an error is uncaught and bubbles to the top level the end result might be the same, but throwing an error allows intermediate code to recover or handle that in custom ways.

4 Likes

To me, it seems reasonable to have a mechanism for @main to generate a non-standard entry point like *WinMain, and part of that mechanism could be to control what the signature of the Swift-level main is and how errors and/or return values from the Swift main get handled in the system entry point.

1 Like

…and the benefit of libraries is that we can use them to shape the language as we desire:

protocol EntryPoint {
    // This function doesn’t qualify for @main and is ignored
    static func main() -> Int
}

extension EntryPoint {
    // This one does
    static func main() {
        let result: Int = main()
        exit(Int32(result))
    }
}


@main
struct MyEntryPoint: EntryPoint {
    static func main() -> Int {
        print("Hello, World!")
        return 0
    }
}
6 Likes

Okay, found some time to read through responses and come up with some responses of my own.

Discussion Summary

Pro:

  • Windows has multiple exit functions with subtly different program shutdown behaviors. Returning from main allows the Windows runtime to determine how to exit, so that we don't have to reverse and re-implement that behavior.

ExitProcess and exit behaviour is subtly different in that module destructor ordering is inverted between the two. - @compnerd

  • Odd runtime implementations don't necessarily call exit and kill the process after the child returns from main, as in the case of the AWS lambda runtimes. If they want a return code, they cannot call exit.

I currently wrap the Swift Lambda runtime’s main with my own to set up some additional error handling while still delegating to the existing behavior, and if that wrapped method were to explicitly call exit instead of returning or throwing, I would lose the ability to do any final cleanup after that main returns. - @MPLewis

  • This doesn't add or remove anything from the program. If you don't use it, it doesn't affect you. If you do need it and are operating in a weird environment, you have a way to plumb the exit code out of the program without additional assumptions baked in*.

*Note: The async main static func main() async implicitly calls exit to ensure that the runloop stops when the program stops. Without it, the dispatch_main runloop will not stop at the end of execution and the program does not terminate. I have some initial ideas for how we can deal with this.

Cons:

  • Doesn't generalize to top-level code

I would hate for someone to prefer @main over top-level code because they wanted to return EXIT_FAILURE - @jrose

  • Spelling, should be a error type, and Error should be extended to contain the exit code value.

I’d strongly prefer something like main() -> Error? , if there’s any way to make that work. - @David_Sweeris

  • Many languages don't have return codes. Java, Python, etc, which may make it more difficult for newcomers to understand what is happening.

I admit that many (all?) non-C languages (at least golang, python, rust off the top of my head) do force you to type out the explicit exit , but I'm not sure it really adds significant value over just returning an exit code from main. - @compnerd

Response

Exit Function:

On most systems the C runtime will call _exit shortly, or immediately, after the main function returns.
For these purposes, an exit function is perfectly fine. A few issues with this approach popped up in discussion that I'd like to note. I'm not against an exit function in the stdlib, but it's also not what I'm pitching.

Windows:

Some systems have a single, sane, way to call exit and call it a day. Windows is not one of these systems. There are a number of rules about when you can and cannot call ExitProcess, depending on thread-state, and whether you're using the CRT or not. I believe Swift programs always do, so that simplifies things a bit, but we would still need to ensure that we've called ExitThread on all running threads before ExitProcess gets called.

The primary thread can avoid terminating other threads by directing them to call ExitThread before causing the process to terminate (for more information, see Terminating a Thread). The primary thread can still call ExitProcess afterwards to ensure that all threads are terminated.

Given that exiting is only safe from the main thread, after all other threads have exit'd, would the declaration be @MainActor func exit()?

The other challenge with threads is that in Windows 10 and later, there is a parallel loader involved, which spins up threads that we don't spawn.

I don't know Windows as a platform well enough to implement or even really discuss what a correct, one-size-fits-all exit function would look like on Windows. I'll defer to @compnerd on the details there.

It still sounds like a return and letting Windows implicitly handle it correctly is a less error-prone approach. We're guaranteed that the main function that returns to the host runtime is running on the main thread, and we can let that runtime do the cleanups, which ExitProcess does not seem to do and we would need to implement and maintain.

Composition with runtimes:

The AWS-Lambda environment handles programs in a non-standard way. There are routines that need to run after main returns, but before the process is actually killed.

Not all runtimes have a notion of separate processes where an immediate exit call makes sense.
I've seen some embedded environments that are less of an OS and more like a collection of routines getting called in a while loop. There is no sense of process. Each program is calling _main and grabbing the exit code it returns, more like a normal function call. There is no notion of _exit since there are no processes. I suppose I could concoct something in asm with the appropriate move's and jumps? I'd prefer to just return though.

Top-Level code:

After some offline discussions, we could have free-floating return statements in top-level code. There is precedent for control-flow in top-level code in that we have try and the other constructs in top-level.

e.g.

let a = getAnA()

guard a < 100 else {
  return 100
}

return a

That leaves a question on whether we stay consistent with closures and functions and automatically return the value of a single-expression function.

e.g. does this program return 42 or 0?

42

It looks weird, but I don't completely hate it either.
Thanks for this idea, @Douglas_Gregor.

Throw/return an Error with a return code:

Starting with func main() -> Error?; I don't think this spelling makes sense. At that point, if Error has an exit code associated with it, we should think about making func main() throws not emit a stack trace on exit, or have a flag to control it, and use the throwing variant. This seems like a totally reasonable direction for the throwing variant. If it doesn't, I'm not sure what this means. Even if we managed to keep the memory alive after our runtimes have been taken down and memory free'd, the OS/runtime/loader are unlikely to understand what a Swift Error is.

I don't think that this should be the only approach though. Not all environments see return-codes as strictly indicating an error state, but more like a C enum. Just like it doesn't make sense to replace our enums with Errors, I don't think it makes sense for this to be the only option.

Pedagogical Issues:

I don't have a super complete answer to this since everyone learns differently and folks can rebut almost anything I say here. For most development, folks won't need to think about the return value, just like how folks don't generally need to understand the runloops behind app delegates, or how the swift runtimes are brought up. I'm not replacing the original behavior, just extending it, so this pitch shouldn't have any impact on the progressive disclosure of the language. As you're learning the language, you can write exactly the same code as you would write today. The difference is, if you find yourself in a position where you need to return a non-zero/non-one value to the command line, that option is available without having to pull in C.

3 Likes