SE-0281: @main: Type-Based Program Entry Points

The review of SE-0281: @main: Type-Based Program Entry Points begins now and runs through April 8, 2020.

Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager (via email or direct message in the Swift forums).

What goes into a review of a proposal?

The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift.

When reviewing a proposal, here are some questions to consider:

  • What is your evaluation of the proposal?

  • Is the problem being addressed significant enough to warrant a change to Swift?

  • Does this proposal fit well with the feel and direction of Swift?

  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Thanks,
Tom Doron
Review Manager

16 Likes

Extending this capability to third-party developers is, by itself, an exciting and worthwhile goal. But if UIKit and AppKit adopt it, allowing us to rip out ~100 of my least favorite lines in SILGen, I will be thrilled.

Verdict: Pretty please with sugar on top.

14 Likes

Definitely headed in a good, logical, cross-platform direction. Makes Swift consistent, and makes it more obvious where the magic bootstrapping code is hidden. I like it.

But, what happened to the @entryPoint suggestion, as opposed to main() being the sole spelling?

Read the pitch thread, and the proposal.

1 Like

It is mentioned in the alternative section:

Alternatively, a future proposal could add an attribute that would let a library designate a different symbol name for the entry point…

+1. This proposal generalizes the facilities that have been previously available to Apple-only frameworks, and makes these facilities available to user-defined libraries.

The discussion about forwarding arguments from the platform entry point to the Swift entry point was compelling, however, it seems like it is solving a separate issue than what this proposal attempts to solve. I think providing access to those arguments, or defining arbitrary entry points with a given platform-specific name should be discussed as a separate proposal -- I don't think the outcome will affect the design of the feature proposed here: we would still want to allow users to define a no-arg main() for convenience.

Apple frameworks have been enjoying the special language features @NSApplicationMain and @UIApplicationMain, so yes, eliminating this type of boilerplate is already considered significantly important.

Existing attributes seem to have been working well, and the proposal is a generalization.

The proposal explores alternatives really well and explains why those were not pursued.

  • What is your evaluation of the proposal?

+1

  • Is the problem being addressed significant enough to warrant a change to Swift

Yes

  • Does this proposal fit well with the feel and direction of Swift?

Yes. I understand non arg option. I remember the first time learning about main and how confusing the arguments part was.

  1. does this work on windows as is?
  2. throwing main support?
  3. how do multiple mains will work (platform specific)
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

C based Langs

  • How much effort did you put into your review? A glance, a quick reading, or an in-depth study?

Read proposal.

I'm strongly against the proposal in its current form.

In the case where a developer supplies their own main function in their own type, it's fine. However, that doesn't seem like the motivating case.

In the case where a framework or library supplies the main function, the following pair of declarations is required by this proposal:

  1. A protocol declaration in the framework (ApplicationRoot in the proposal's example) that has a default implementation of main).

  2. A custom type in the developer's code that conforms to the protocol, and carries the @main attribute.

Note that both of these declarations are meaningless scaffolding. The framework protocol doesn't actually have main as a requirement, and the custom type doesn't exists for any other purpose that to connect @main with an implicit conformance.

The whole things seems arbitrary and convoluted. I'm 100% OK with the goal of abstracting the @main-ness of our current specific solutions, but I'm 100% against doing that with hackery, and IMO this proposal is effectively hackery. (No offense intended towards the author!)

4 Likes

I think this is more trying to generalise an observed pattern, than hackery. In the 3 cases mentioned by the proposal, the protocol is not meaningless scaffolding, but an important part of the Framework (UI/NS ApplicationDelegate for Apple Frameworks, and ParsableCommand in ArgumentParser).

A Framework that provide a main entrypoint must provide an other entry point to the client app (delegates methods in iOS/macOS, run() method in ArgumentParser). Assuming that such framework will define a custom class/protocol that the app will have to "override/conform to" is reasonable IMO.

4 Likes

You cannot launder a comment of its offensive nature by saying "no offense intended" after it. Please try to avoid pejorative characterizations like this.

11 Likes

I'm +1 on the direction and generally +1 on the proposal as a whole, but I have a few questions:

  • Top level code is allowed to throw and has some special behavior for reporting errors etc at that level. Wouldn't it make sense to allow this to carry over here? Perhaps the main() function should be allowed to throw?

  • Similarly, would it make sense for the main() function be allowed to return Void or Never? I don't see a strong reason to constrain to one or the other, both can and should work to mirror the behavior of top level code.

  • The spelling of @main feels a bit weird to me, maybe because I can't figure out whether @main is noun-like or verb-like when juxtaposed with a class name. The alternative considered section makes good points about precedent in other languages for this, but the Swift precedent is the @XXApplicationMain attributes. Would it make sense to pick a name that includes "application" in it or even just closely follow existing precedent and use @applicationMain? Other type-level behavior changing attributes have had longer compound names (e.g. @propertyWrapper, @objcMembers, @dynamicMemberLookup etc), which improve googlability and other things.

  • I would love to see a future proposal to enhance SwiftPM to support this. The magic main.swift thing always seemed weird.

This is a minor improvement, and could lead to cleanup of other parts of the system, so yes, I think it is worth it.

Yes, N/A, read the proposal.

-Chris

6 Likes

+1. I think this is a reasonable incremental improvement, and it generalizes a feature Swift has had from day one to make it usable with other frameworks and platforms. I think I'd prefer a design which addressed platform entry points at the same time to make sure @main doesn't restrict future evolution in that area, but I can understand why it was left out for now.

Yes, I think the existing UI/NSApplicationMain attributes demonstrate that a generalized feature will be nice to have as Swift is used more frequently outside the UIKit and AppKit worlds.

I think it does, especially when considering the fact that the average Swift programmer will write their own main infrequently, if at all, if this proposal is accepted.

N/A

I read the proposal and some of the other reviews.


IMO the main benefit of allowing throwing top-level code is for use in scripts which don't require user-friendly error-reporting. I think it would be occasionally helpful if main() was allowed to be marked throws, but I don't think it should be required.

I agree. The proposal argues that returning Void is preferable because it makes it easier to use without the @main attribute in some cases. However, if I have an implementation of main() that calls, for example, exit(), I would prefer to make it Never-returning so clients don't get the wrong impression.

I agree, I'm suggesting that the entrypoint could be allowed to be marked throws, and maintain the
existing "top level code" semantics within its body if anything throws. I'm not suggesting that an entrypoint implementation be required to be marked throws.

2 Likes

I'm generally in favour of this proposal.

The attribute @main is very confusing as it confuses the main C entry point and the class. I think that an alternate spelling would be preferable.

The handling of alternate entry points is not made clear. Is a change which does something like this be acceptable in the future?

struct ApplicationRoot {
  @entryPoint("entryWithELFAuxiliaryVector")
  satic func entry(elfAuxiliaryVector: UnsafeRawPointer) {
     ...
  }
}

@main
struct Application {
}

This would change the meaning of @main to go from call main to call entry and this would be considered acceptable and not a breaking change?

Yes

Yes

N/A

Spent a fair amount of time trying to understand implications for alternate platforms.

I'm in favor of this, and I think it's helpful to break it into two parts to see why it is the way it is:

  1. A common signature for entry points.
  2. A way to use an entry point for a program without top-level code.

I'll start by talking about #2 first, and I want to make it clear that this is definitely justified. Swift got @NSApplicationMain and @UIApplicationMain in Swift 1.1, so there was interest at the time in getting rid of the vestigial main.swift. There's not much point in knowing that a program starts in main.swift just to see that it immediately enters a library function for the rest of the program, especially if that library function is some kind of event loop; it would be more sensible to have that logic attached to a type that's already relevant.

What I wouldn't want to see is people using this when there's no relevant type (for example, making a dummy type for calling dispatchMain()). But I guess I can't stop people from doing things with a feature that I wouldn't.


#1 is the other interesting part because it kind of gets glossed over. It's certainly possible to write a protocol for this:

protocol ProgramMain /* or EntryPoint, or whatever */ {
  static func main()
}

But then what? This just declares that a type can be used as an entry point; it's still up to an application which type to use as an entry point. The attribute (or some other annotation) is still necessary to make this feature work.

That said, it's a reasonable design for @main to only work on types that conform to ProgramMain. What would the implications of that be? The only one I can think of is that conforming to NSApplicationDelegate wouldn't be enough; AppKit apps would have to make use of both protocols, and likewise for UIApplicationDelegate / UIKit apps.

// Before
@NSApplicationMain
class AppDelegate : NSApplicationDelegate { … }

// After
@main
class AppDelegate : NSApplicationDelegate, ProgramMain { … }

Honestly, that doesn't seem like too much of a burden to me, and so I'd prefer a formal protocol rather than another member name the compiler magically recognizes. (Of course the protocol is still a magic name the compiler references, but it makes it easier to document and to point out what someone's forgotten to do.)


On the notes about types, I agree with @Chris_Lattner3 that we ought to accept throwing main functions, inserting a top-level try before the implicit call if necessary. That costs us nothing. What'd be a little trickier is accepting Never-returning main functions. I think the current design with Void-returning main is a good one, not in the least because it's the simplest thing for someone to write when they haven't learned about Never yet, but it would be nice™ if Never were accepted. Unfortunately, doing that with a protocol is clunky:

protocol ProgramMain {
  static func main() throws
  static func main() throws -> Never
}
extension ProgramMain {
  public static func main() throws {
    try main() as Never
  }
  public static func main() throws -> Never {
    try main() as Void
    exit(0) // or equivalent
  }
}

Without extra compiler support, this means someone could conform to ProgramMain without implementing either method and run into an infinite loop. Oops.


As for the name, I do think @main is a little ambiguous on a type (does being the "main struct" mean it's the most important?), but I tend to agree that the precedent in other languages makes it worth it for both the attribute and the entry point itself.


EDIT: I missed this on my first read

The @main-designated type can be declared in the application target or in an imported module.

and definitely think putting @main in a library should not be allowed. While @main on a type in your own code does require you to check the type's inheritance to see who's responsible for starting the program, pulling in @main via imports means a reader of the code would have to check all imports, transitively, for whether they have an entry point type. And if two imports have an @main, which one wins?

2 Likes

This is a point that I hope will be addressed more substantively before the review closes.

The NS/UIApplicationDelegate protocol doesn't really provide any kind of entry point or top level code for the app. The protocols have a loose relationship with "startup" only because the Xcode template provides a stub application:didFinishLaunching… implementation that, if used for anything, runs before the app's UI appears. No app delegate is actually required, and if present it's not required to do anything "at startup".

The only reason — I would claim — that we have @NS/UIApplicationMain on the app delegate is that a custom delegate subclass is almost always present (due to the project template, if for no other reason), and it's the only user-defined class that's almost always present. In other words, there was no other reasonable place to put the @NS/UIApplicationMain attribute.

In other words, it was something like a forced moved. It's good there is a solution, but it doesn't make the solution logical.

If we were designing and implementing AppKit or UIKit now, and wanted those frameworks to provide their own main() function, it wouldn't be logical — I would claim — to implement as a static method in the delegate class. It might make sense to make it a static method of the NS/UIApplication class but that would be inconsistent with this proposal, because it would force the app developer to subclass NS/UIApplication unnecessarily.

That's the reason I've been making a fuss about this. It seems to me that we're enshrining an illogical connection in a new generalized behavior, and I would hope that there could be some substantive discussion of this issue before it's too late.

3 Likes

NSApplicationMain might follow that rule—it's a lot like dispatchMain in that way—but UIApplicationMain does not. UIApplicationMain takes the delegate class name as an argument, and so the implementation would be something like this:

extension UIApplicationDelegate {
  @inlinable
  public static func main() {
    UIApplicationMain(CommandLine.argc, CommandLine.unsafeArgv, /*custom application class name*/nil, NSStringFromClass(self))
  }
}

I think the reason why I'm okay with extending this is because, well, obviously calling a library function can't be the only thing you need to do to customize your app. There must be additional configuration coming from somewhere:

  1. There's additional setup that happens before the call to the library-provided "main" function. In this case, you can't get around writing some explicit code; whether that's in a main.swift file or an overridden main() method is technically up to you. I'd put dispatchMain() in this bucket in that if you call dispatchMain() first thing upon entering a program, nothing useful will happen.

  2. The library-provided "main" function reads information from non-code data. This is how NSApplicationMain(_:_:) works: by relying on the principal class and main nib file specified in your app's Info.plist, a resource (data) found relative to the app.

  3. The library-provided "main" function takes extra arguments for customization purposes. The extra arguments don't have to be types, but they can be. I'd put UIApplicationMain(_:_:_:_:) in this bucket.

I don't think I would say any of these approaches are inherently superior to the others in all cases. (2) seems weakest to me because code-based tools like the compiler may want to reason about your non-code data, but that doesn't mean it's totally flawed. I consider this proposal to be a way to make (3) more convenient for the common case of "I can put all my customization in one type"; it's sugar, but it's clearly-defined sugar.

To your point, though, will it cause people to make types where a type wasn't really needed? I had the same concern, and I can definitely imagine people writing this for a hypothetical web server framework:

public protocol Server {
  static func setUp(_ requestManager: RequestManager)
}
extension Server {
  public static func main() {
    let requestManager = RequestManager()
    setUp(requestManager)
    requestManager.startHandlingRequests()
  }
}
@main
struct MyServer: Server {
  static func setUp(_ requestManager: RequestManager) {
    requestManager.handleGET("/", getRoot)
    requestManager.handleGET("/version", getVersion)
    // …
  } 
}

when the client could have just written

let requestManager = RequestManager()
requestManager.handleGET("/", getRoot)
requestManager.handleGET("/version", getVersion)
requestManager.startHandlingRequests()

But I think ultimately the ability to write default implementations and such for "how to enter the program a normal way" is a good one; that's why NSApplicationMain itself exists, after all. And if you do have a top-level type (or something close to it like a delegate), then those default implementations can come from protocols the type would conform to anyway.

1 Like

Indeed! Adding this kind of @entryPoint attribute would be a separate proposal. As long as the design of the feature doesn't break existing code using @main (and I don't see why it would have to), then that could be an acceptable change.

Right, my concern is that as it currently stands, I don't see how it would not be possible to break existing code to get access to the new function. In that example, I am suggesting that the code would be equivalent to:

Application.entry(elfAuxiliaryVector: ...)

The nice thing with the type wrapper approach is that the Application structure could be initialized by the wrapper and the method invoked by rather than having the static method which is responsible for replicating the startup (since you do not have the derived nominal type, and I don't believe that Self() is acceptable in the default conformance) is that this is then possible to accomplish with the @entryPoint attribute.

We'd get that for free if Never were a true bottom type; the protocol would have only one requirement (static func main() throws) and the conforming type could implement the requirement with a never-returning main. This would happen organically and orthogonally without needing to be initially supported, whenever the "bottom type"-ness of uninhabited types is fleshed out.

3 Likes

We'd need that and SR-522: Protocol funcs cannot have covariant returns, but yes, good point.