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?