Exit Codes From Main

I'd like to pitch extending the MainType main function to return an exit code. Returning a non-zero/non-one exit-code from Swift in a portable way is pretty difficult in Swift today.

As a little background for those who are not familiar, exit codes/return codes from a program can indicate exits from a successful program run, exits from a failed program run, or other information. See git bisect run for instance, which uses 0 to indicate that a commit is good, 125 to indicate that a commit should be skipped, and other non-zero/non-125 values to indicate that the commit is bad.

The easiest way, though not necessarily portable, to control the exit code is to import the appropriate libc implementation and call the exit function.

#if canImport(Darwin)
import Darwin   // Apple platforms
#elseif canImport(Glibc)
import Glibc    // GlibC Linux platforms
#elseif canImport(CRT)
import CRT      // Windows platforms
#endif

@main
struct MainType {
  static func main() {
    exit(123)
  }
}

For what it is doing, this is a lot, and it isn't portable. If you want to be somewhat portable, it's easiest to define a header-only "exit" library that contains a single header declaring extern void exit(int); (and the appropriate module.modulemap so you can import it, of course). That works fine unless you're working with SwiftPM, which can't deal with header-only C libraries, so now you end up with a libExit.a that is an empty library, just so you can call exit. This is a pretty hilarious excursion if you're just trying to write a little script. It's a little better if you're using Foundation and friends since those re-export the exit function, but that's not exactly a small framework.

In either case, there is way too much work involved in returning an exit code.

Proposed Solution

I'd like to propose adding the following function declarations to the accepted list of

  • main() -> CInt
  • main() async -> CInt
  • @MainActor main() -> CInt
  • @MainActor main() async -> CInt

Instead of having to call exit, or call through another library, this will allow the main function to return directly.

@main struct MainType {
  static func main() -> CInt {
    return 123
  }
}

We will still accept a void-returning main function. The return of a non-throwing, void-returning main function will continue to be 0.

Details

The overload resolution behavior would behave the same way as the other main function types.
The declaration in the most specific conformance is the function that gets selected.

In the example below, MyMain conforms directly to ReturningMainProtocol. The main function defined in the ReturningMainProtocol is the most specific function that applies to MyMain, so that is the main function that gets chosen.

protocol MainProtocol { }

extension MainProtocol {
  static func main() { }
}

protocol ReturningMainProtocol : MainProtocol { }

extension ReturningMainProtocol  {
  static func main() -> CInt { return 42 }
}

@main struct MyMain : ReturningMainProtocol {}

In the event that there are multiple main functions with an acceptable type, the compiler emits an error message indicating the ambiguous functions declarations.

protocol MainProtocol { }

extension MainProtocol  {
  static func main() -> CInt { return 42 }
  static func main() -> Void { return } // Error: ambiguous `main` declaration
}

@main struct MyMain : MainProtocol  {}

As part of this change, I would like to address the @main return type. The @main entry-point returns a hard-coded int32. This function doesn't exist at a source level and is generated by the compiler at the SILGen layer, but if it existed, the flow of a normal synchronous program startup would look a little like this snippet here:

struct MainType {
  static func main() { } // The actual main function as seen in source

  static func $main() { // Generated by the compiler
    MainType.main()
  }
}

// Generated by the compiler
@_cdecl
func @main(_ argc: Int32, _ argv: UnsafePointer<UnsafePointer<CChar>> ) -> Int32 {
  // Do some hidden initialization work
  // ...
  MainType.$main()
  return 0
}

The Int32 works on most systems today because C's int type is usually 32-bits on both 32-bit systems and under most 64-bit data models, but it is not technically correct. They should be CInt types to reflect the bit-width of the C int type.

The end result would look more like

struct MainType {
  static func main() -> CInt { return 32 } // The actual main function as seen in source

  static func $main() -> CInt { // Generated by the compiler
    MainType.main()
  }
}

// Generated by the compiler
@_cdecl
func @main(_ argc: CInt, _ argv: UnsafePointer<UnsafePointer<CChar>> ) -> CInt {
  // Do some hidden initialization work
  // ...
  return MainType.$main()
}

Source Compatibility

This will break current valid Swift programs that define a separate CInt-returning main function, like the following:

import ExitLib
@main struct MainType {
  static func main() {
    exit(MainType.main())
  }

  static func main() -> CInt {
    // Compute the Answer to Life...
    return 42
  }
}

This is well-formed today and will result in a program that exits with 42 as the status code.
With the proposed solution applied, both declarations are considered valid main function types, so Swift will complain about an ambiguous function overload.

CInt is a typealias defined in the Swift standard library. As such, it is interchangeable with the underlying type. On most systems today that makes it an Int32, so a program that defines a main function that returns an Int32 will fail to compile on systems that have a 32-bit integer as the C int type.

ABI Compatibility

Fixing the underlying @main function return type to return a CInt instead of hard-coding an Int32 is almost ABI-breaking, but we can get away with it. Why? Because CInt on Darwin, where we guarantee ABI stability, is defined to be Int32, so that continues to work. On platforms where CInt is not an int32, the world is currently broken anyway. From a symbol name perspective, the @main is a CDecl, so the name is simply _@main and does not include the parameter or return type in the mangle, resulting in no breakage by changing the return type.

Open Questions For Discussion

There is one point that I've glossed over. Throwing main functions today will return with 1 if the main function threw an error, 0 otherwise.

  • Do we want to allow a throwing main function to return an explicit status code, or is the current behavior sufficient?
18 Likes

I would personally advocate for excluding static func main() throws -> CInt and static func main() async throws -> CInt entirely - if someone wants to take control of the exit code, I’d prefer to make them to do their own error handling so there’s no confusion as to what exit code means what.

6 Likes

Do you mean "a main function that returns an Int32 will fail to compile on systems that do not have a 32-bit integer as the C int type"? Or even "will continue to compile"?

Is this something we would add to Swift if C didn’t exist? I’ll note that even though C’s main returns int, and exit takes an int argument, exit codes aren’t actually int or Int32; POSIX uses

The low-order 8 bits of the status argument that the process passed to _Exit(), _exit(), or exit(), or the low-order 8 bits of the value the process returned from main()

But Windows documentation just says

the value in status is made available to the host environment or waiting calling process

int is also signed, but exit statuses on both POSIX and Windows are treated as unsigned (due to the truncation for POSIX, and the ERRORLEVEL check on Windows). In a clean environment, would we use CInt or Int32 to represent an exit status?

I’m being a bit facetious here, but really I think we should just add exit to System and call it a day. I would hate for someone to prefer @main over top-level code because they wanted to return EXIT_FAILURE. (I realize that’s a matter of opinion, but it’s also something the project seems to care about, cf. SE-0343 Concurrency in Top-Level Code. EDIT: also from you, heh.)

18 Likes

That part could probably be re-worded.

It was meant to be an extension to the code snippet above it

import ExitLib
@main struct MainType {
  static func main() {
    exit(MainType.main())
  }

  static func main() -> CInt {
    // Compute the Answer to Life...
    return 42
  }
}

That would fail because static func main() -> Void and static func main() -> CInt are acceptable main function declarations.

import ExitLib
@main struct MainType {
  static func main() {
    exit(MainType.main())
  }

  static func main() -> Int32 {
    // Compute the Answer to Life...
    return 42
  }
}

This would also fail on macOS for the same reason, because CInt is a type alias of Int32, so static func main() -> Int32 is a valid main function declaration, and it's ambiguous whether you wanted static func main() or static func main() -> Int32.

1 Like

That's kind of the direction I've been leaning, yeah.

Could the ArgumentParser.ExitCode type be added to the standard library?
Then the @main type, or a top-level script, could throw a standard or custom exit code.

7 Likes

I feel like this should not be an error type, but a protocol. Tool authors can bind error types to custom exit state.

However, if we really make it a protocol, we should also clarify that library authors are not suggested to do so as this could possibly lead to collisions with other libraries.

That's my feeling as well. There isn't really anything concretely special about returning from main from any contemporary OS's perspective; in practice, main is going to return into crt stub code that's equivalent to passing the return value to exit(3) to tear down the process anyway.

8 Likes

@jrose

EDIT: also from you, heh.

I noticed that too. :slight_smile: That proposal was an extension of the language to bring concurrency to top-level, after the @main version had it though.

Is this something we would add to Swift if C didn’t exist? I’ll note that even though C’s main returns int , and exit takes an int argument, exit codes aren’t actually int or Int32

In a purely Swift world, we'd likely use Int. @Joe_Groff has also brought this up offline. Given that different shells and environments truncate the result differently I'm thinking the overload could be something like main() -> Int and then we quietly truncate it to a CInt for the implicit @main function so that crtend.o handles the final result properly.

I think we should just add exit to System and call it a day.

That would work too. That would effectively end up being a call to the C exit function, wouldn't it?

@benrimmington

Could the ArgumentParser.ExitCode type be added to the standard library?
Then the @main type, or a top-level script, could throw a standard or custom exit code.

We could probably do something like this from a throwing main. I think there are some challenges to that in terms of cost and complexity. We would need to inspect the thing being thrown, ensure it conforms at some point to ExitCode, and fish out the value to return. In the case that an error is thrown, but isn't a ExitCode, what would we want to do?

Maybe it's the C in me talking, but exit(15) or return 15 reads nicer than throw ExitCode(15).

@stevapple

I feel like this should not be an error type, but a protocol. Tool authors can bind error types to custom exit state.

You're going to need to provide an example. I'm not sure what you mean.

@main struct MainType {
  static func main() -> Int {
    guard openFile() else {
      return 1
    }
    guard doStuff() else {
      return 2
    }
   return 0
}

How would something like this with protocols?

i like the current convention where throwing from a top level function makes the program return 1. i don’t think there’s much value in allowing for non-boolean return codes, since that reduces portability.

one problem with top level errors is they always print a full stack trace when the program exists, and sometimes i want to print something more user-friendly. if there were a way to customize the error output, i would not need to return exit codes from main.

1 Like

This is how I'm leaning as well. Exit statuses other than simple success/failure are likely to either be for inter-process communication with processes you own, where you also want to be able to exec/waitpid/etc., or for communication with processes you don't own, which is going to be platform-specific. This feels like a good case for swift-system. Another advantage is you could easily add a way to call _Exit and maybe even atexit as well. Incidentally, exit is on the swift-system api roadmap.

Failing that, I also kind of like the idea of adding an error protocol with exit status.

3 Likes

We could potentially extend the existing Error protocol with a new defaulted requirement:

protocol Error {
  var processExitCode: Int
}

extension Error {
  var processExitCode: Int { return 1 }
}

and have it so that errors that propagate from main/top-level code end the process with the error value's processExitCode. I think it'd still be useful to have a standard standalone exit(_:) function too, though.

That might also be a useful extension point on Error, we could have a presentTopLevelError() method that defaults to the stack trace but which user errors can override with nicer presentation logic if they want.

9 Likes

Not exactly. Returning from main will only terminate the process if there is no remaining detached thread running, unlike calling exit() which will terminate the process immediately.

At least on Apple platforms, main() literally returns into a call to exit(3) inside dyld:

so exit(3) will also leave background threads running while tearing down the main thread (much to our occasional chagrin when a critical runtime data structure accidentally grows a C++ destructor). The C and C++ standards also specify that returning from main must be equivalent to calling exit(3) with the return value. _exit(2) is the underlying system call that summarily kills the process.

3 Likes

I'm stand corrected. Thank you for pointing that out :slightly_smiling_face:

1 Like

I'd like to suggest a slight variation: add it to Swift.CommandLine, which is currently where you go to retrieve arguments.

extension CommandLine {
    public static func exit(withCode code: Int) -> Never
}

CommandLine.exit(withCode: 42)
10 Likes

I like the idea of making it available in the standard library, but CommandLine seems like a strange place for it, given that most users don’t launch GUI apps via the command line yet exit codes are still potentially relevant for GUI apps (e.g. for Mac App Store receipt rejection).

2 Likes

I agree returning from main is morally equivalent to calling exit. I do question if being explicit about the exit code is adding clarity to the program or not. 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.

One small problem arises about portability of wrapping exit though. I believe on Windows, ExitProcess and exit behaviour is subtly different in that module destructor ordering is inverted between the two. I really would like to see a singular spelling across the platforms, but with some sort of discriminator to select between the different variants.

I'm mostly fine with an exit function. I don't like the idea of it being in a separate library; having to import and link an extra library just for a return code seems excessive. Especially given that the thing we eventually want to call is already linked into the program without linking an additional library. As @compnerd says, we would need an answer to the exit vs ExitProcess on Windows. I'm not familiar with Windows or these APIs enough to have a meaningful opinion on that front.

I'd still like to understand more of the aversion to having a returning main though. I wouldn't mind extending the pitch further to the full main(args: [String]) -> Int like described in the original proposal for @main: SE-0281 main-attribute.

Is it just that it doesn't generalize to top-level code and other code, or something else?