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
beEXIT_FAILURE
? - Should
presentTopLevelError()
have astderr
parameter?
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.
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.
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.
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.
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.
ā¦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
}
}
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 frommain
allows the Windows runtime to determine how to exit, so that we don't have to reverse and re-implement that behavior.
ExitProcess
andexit
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 callexit
instead of returning or throwing, I would lose the ability to do any final cleanup after thatmain
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 returnEXIT_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.