Calling async from main()

The Swift Book says that

The possible suspension points in your code marked with await indicate that the current piece of code might pause execution while waiting for the asynchronous function or method to return...Because code with await needs to be able to suspend execution, only certain places in your program can call asynchronous functions or methods:

  • Code in the body of an asynchronous function, method, or property.
  • Code in the static main() method of a structure, class, or enumeration that’s marked with @main.
  • Code in an unstructured child task, as shown in Unstructured Concurrency below.

However, the following simple example does not compile (see compiler error at the botom) unless main() is also async-annotated:

import Foundation

@main
struct S {
  static func main() {
    let session = URLSession(configuration: URLSessionConfiguration.default)
    guard let url = URL(string: "http://www.google.com") else {
      return
    }
    let (_, response) = try! await session.bytes(for: URLRequest(url: url))
    NSLog("Received response")
  }
}

Is the book outdated/wrong or am I doing something wrong? The book made it sound like @main was treated specially—perhaps as the program's entry point—but maybe that's inaccurate and it needs async too?

Compiler Error

error: 'async' call in a function that does not support concurrency
35 | @main
36 | struct S {
37 |     static func main() {
   |                 `- note: add 'async' to function 'main()' to make it asynchronous

Yeah, I think that is wrong. If you were to write your code in a main.swift-style entry point, rather than @main-style, then it would work as you expect. So maybe that is the special case the book means to call out. It looks like in early drafts of the book this was properly called out, and later revisions simplified the language but perhaps at the cost of correctness.

1 Like

My code is in main.swift (the only file in my Package)

I tried keeping in main.swift just

func main() async throws {
...
}

but I get a linker error:

Undefined symbols for architecture x86_64:
  "_foo_main", referenced from:
      _main in command-line-aliases-file
ld: symbol(s) not found for architecture x86_64

I tried renaming the file to something else but then I get

35 | @main
   | `- error: 'main' attribute cannot be used in a module that contains top-level code
36 | struct S {

If I just leave the function in a.swift

func main() async throws {

then the project builds but nothing runs (it's a library, I guess, not an executable with an entry point).

I agree that the description in the docs is a bit misleading, but the compiler error you quote does say what you have to do:

Notice the note: "add 'async' to function 'main()' to make it asynchronous"

So async code in static func main() will work as long as you make your main func async:

@main
struct S {
  static func main() async {
    …
  }
}

As for the different ways to compile a Swift executable on the command line, this can also be pretty confusing and I don't know a place where this is documented in an end-user-friendly way: you have 2 options:

  1. Name your file main.swift and compile it as is. Do not add a func main() or static func main(), just write your program as top-level code. You can also freely use await and/or try in top-level code. The compiler is smart enough to create an async-throwing context for you.

    $ swiftc main.swift
    $ ./main
    
  2. Name your file anything but main.swift (because the compiler treats main.swift as top-level code) and create a @main struct that contains a static func main(). If you want to use await or try in your main func, you can and must annotate it with async and/or throws.

    Now you must also pass the -parse-as-library flag to the compiler. This tells the compiler not to expect a main.swift file.

    $ swiftc -parse-as-library a.swift
    $ ./a
    

When you use SwiftPM, I believe SwiftPM will automatically do the right thing: if your executable target has a main.swift file, SwiftPM will treat the top-level code in it as the entry point. If you don't have a main.swift file, SwiftPM will pass the -parse-as-library flag and this will tell the compiler to look for a @main struct as the entry point.

2 Likes

The name [of the file] does not seem to matter (well, at least in my simple case where I have a single file in the package). One can't return from a top-level/non-function code so I had to write something ugly like the following but it did compile and run:

extension Int: @retroactive Error {}

let session = URLSession(configuration: URLSessionConfiguration.default)
guard let url = URL(string: "http://www.google.com") else {
    throw 1
}
let _ = try await session.bytes(for: URLRequest(url: url))
NSLog("Received response")

I think you're right. SwiftPM new-ish "simplified" one-executable-target package format, where the source code is directly under Sources, not in per-target subdirectories under Sources, seems to always assume top-level code, regardless of filename.

(I don't remember when this simplified package format was introduced. Or did it always exist and only the default template for swift package init --type executable changed? Is there a Swift Evolution proposal for it?)

If you want to use an @main struct, it seems you have to put your target's code in Sources/MyTarget/a.swift (or similar).