Why is there so much boilerplate to declare a main function?

hmmm, I'd imagine that would be the very purpose of "@main" - to mark something as the main entry point, something that can be arbitrary named, would be logical no?
Not saying we should do that though.

To some extent I'd agree, it's almost like a different language with some special rules. Being mainly an app developer for apple platforms that's typically not my concern though, I only start seeing this oddity when it comes to standalone console apps, or playground.

I think it could be confusing, since a @main type requires the method to be called exactly main, so you'd have different rules for main functions and main methods.

I'm not trying to argue either style is superior, just that this limitation doesn't seem to follow from any technical reasons. It feels like a consequence of how @main was designed which is described in this proposal.

From what I can see, the idea was to provide an alternative to top level code as the entry point, and most importantly to allow frameworks to define the main function, not to allow the programmer to define the entry point. I don't think allowing a C style approach to main() was considered, leaving only a very object oriented main type as the default.

I feel like the current implementation of @main leaves out procedural code a little bit :slightly_smiling_face:

From the perspective of C interoperability, low level libraries are designed with C in mind, with the expectation that they will be used as a series of function calls. In this context, having a type to contain main() has a noticeable downside of indenting everything twice and reducing the amount of space with no real upsides.

not object-oriented, just namespaced, because main is static.

this is true pretty much everywhere in the language.

Part of the issue here might be that @main was designed primarily as a way to get entry point behavior provided by a library or framework. Like in SwiftUI, if you write @main struct MyApp: App, you're generally getting the main method that App implements for you and not implementing it yourself, and you're saving some boilerplate overall because the other members of MyApp describe how the program should run in terms of the framework-provided entry point. But maybe we could also let you throw @main on a top-level function, if you just want to have an entry point function totally under your own control.

7 Likes

That would be nice. :slight_smile:

But, until then I will keep using this:

// MyMain.swift

@main
struct MyMain {
   static func main () async {
       // main code here
   }
}

This also dovetails with the exit status conversation. It would be nice to be able to simply wrap my top level code in @main func main() -> Int when I decide my script needs to exit with a status code.

3 Likes

The template probably assumes you will put your main function in a non-executable target and therefore needs to be public. The main.swift file is more of a historical decision that is less preferred now. I think it was originally intended to act more like a scripting language since Swift wants to fit many niches. You can even put a shebang line at the top of a Swift file then run it as a script. Unfortunately, Swifts non-binary form evolves too fast for this to be practical.

If you want to write less code for the main entry point, use a package such as swift-argument-parser to abstract much of the work away. The idea is you are unlikely to write your own entry points in to your code because someone else probably did a better job then packaged it up as a library.

Swift is a multi-paradigm language, so you are free to pick what works for your use case. For the most part, you can learn one paradigm at a time–which helps the learning curve–then pick the best tool for the situation.

Swift is currently a high-level language, but it is picking up features that will allow it to be used for systems programming that need very low overhead (often called zero cost abstractions). Traditionally you would have to pick one language as the high level (such as C# or Java) and another language for systems programming (such as C++ or Rust) then build a interface between the two. Swift aims to do everything in one language.

This is arguably a bug. You can also pass -parse-as-library to parse a single file without going into single-file mode.

The top-level variable behavior was a language design decision at one point. It's a bug now and conveniently all of the forms of this bug seem to be assigned to me. It is on my queue, I'm aware of it, and I do intend to fix it some day since people seem to poke me about it at least once a week.

It gets really weird once you bring classes into the mix because the variable gets default-initialized to a nullptr under the hood, so you segfault if you use it before the declaration/initialization. There are a ton of foot-guns here though because, like you showed, you can defined a function somewhere in the module that uses the global symbol. But unlike globals, which are lazily initialized, these things are sequentially initialized like locals, so if you call that function holding it like a global before the variable gets initialized, you'll have all sorts of fundefined behavior. It's great how the front door of the language is literally where the easiest memory safety issue shows up. :smile:

The @main was sort of geared toward libraries implementing a main function, then you being able to use protocol to select a variant based on what your MainType conforms to. In the end, we end up with something that smells frighteningly similar to Java's public class Main { public static void main(String[] args) { } }.

If you're working with an editor that has snippets/templates, I highly recommend setting something up where you can type @main, hit tab or expand or whatever you select and have it fill out the full entry point. I also have an @maina and @maint for the async and throwing variants.

@ksluder

It would be nice to be able to simply wrap my top level code in @main func main() -> Int when I decide my script needs to exit with a status code.

So in the response, I posted Doug's suggestion. We could totally have it so that you just throw a return # in your top-level code and we could deduce that you want the returning variant. Then you don't have to say anything, just return. :slight_smile:

if blarpy() > 10 {
  return 10
}
return 0

Now it gets weird with single-expressions. 42 becomes a whole program that returns 42. :laughing:

(Edit, added @ksluder comment as a quote at the top of my response)

2 Likes

This is why I mentioned that my perspective is using Swift as a systems language. I wouldn't expect for example a Swift library that provides abstractions for a C library like X11, not even just Swift bindings; It's not something that requires it.

I don't think the only right way to use the language should be through a framework, or making a framework.
I think the features and syntax of Swift still make it a great language to use even without any high level abstractions, there's just some decisions (like the entry point) that were not made with this side of Swift in mind :)

1 Like

If you already have a script, the lower-friction way to add an exit code seems to me to put a call to exit. Adding an entry point function could on the other hand be less friction if you have a block of "library" code you want to turn into a standalone tool without reorganizing the code to make room for a main.swift.

It was never really a decision per se, it got implemented and left this way in the early days, and you're the person who rose to the occasion to fix it. (Which is much needed, thank you for doing so!)

3 Likes

The other discussion makes clear that exit vs. _exit vs. ExitProcess is a nuanced discussion. The meaning of return-from-main is something that Swift can define for itself, and there’s a consistent behavior across all currently-supported platforms.

1 Like

Didn't @jrose write a blog post on it, later to say that it was a mistake?

If you already have a script, the lower-friction way to add an exit code seems to me to put a call to exit.

The more I read about other platforms, the less I think an exit function is a good idea. It's generally an anti-pattern in a library. It only seems to be consistently safe from the main thread (@MainActor func exit() intensifies). Where threads are involved, you really have to make sure that you've already killed your threads before calling it (it being exit(3)).

3 Likes

But as it stands today, returning from Swift main is returning from C main, and returning from C main is equivalent to calling exit.

The mistake (IMHO) wasn’t “having top level code”, it was “things declared in top level code are visible in other files, and stored variables are still lazily-initialized, preventing the compiler from enforcing ordering like it usually does”. I don’t think this aggressive view is universally shared, even among people who want to improve the current situation.

The original motivation was to make it easier to go from a single-file script to a multi-file module, but even then you’re mostly factoring things out of the main script.

3 Likes

Sorry, I should have been clearer. I was meaning the part on variables in top-level code being a Global/Local hybrid thing specifically, not top-level code entirely.

2 Likes

This isn’t the whole story on Windows. The VCRuntime source is bundled with Visual Studio; you can see the full implementation of __scrt_common_main_seh() in exe_common.inl. In certain circumstances, the entry point calls _cexit() instead of exit().

Regardless of what returning from C main does today, Swift is free to define (perhaps based on language version) what it means to return from @main, which sidesteps the question of what process-terminating function is the best to call.

2 Likes

I agree that Swift can define what it means to return from the @main annotated entry point. However, it does not side-step the question of what process terminating function to invoke. The only way to side-step that question is to return from main which Swift does participate in. The Swift runtime uses C++, and thus C. More importantly, for the interop with the language, we need to integrate into the execution path for the language. As a result, we do need to handle the return from main properly at some level, even if it is not spelt the same in Swift.

2 Likes

The special behavior in vcruntime’s entry point(s) exists to handle executables that mix C/C++ with .NET. Swift programs with C++ interop seem to me to fall into the same category. Swift has to cooperate with all the standard C++ startup and termination semantics, but if your entry point is written in Swift, the runtime doesn’t necessarily need to call exit() as long as it faithfully invokes all the C++ termination logic that exit() does.

Which then has the problem of having to cross-reference the various versions of the standard library that you are running against (remember the internals are not guaranteed stable) and ensuring that you have each version's paths mirrored faithfully. Conforming to the proper semantics instead lets you delegate the responsibility to the system (and vendor) which makes it more robust and easier to deploy across multiple versions.

1 Like