Swift Digest
SE-0281 | Swift Evolution

@main: Type-Based Program Entry Points

Proposal
SE-0281
Authors
Nate Cook, Nate Chandler, Matt Ricketson
Review Manager
Tom Doron
Status
Implemented (Swift 5.3)

01 何が問題だったのか

Swift プログラムの実行は、基本的にファイルの先頭から順にトップレベルのコードを実行することで始まります。単純なスクリプト的なプログラムではこれで十分ですが、GUI アプリケーションのように「起動してイベントループに入り、終了するまで動き続ける」タイプのプログラムには、このモデルがうまく合いません。

そのため UIKit や AppKit では、起動手続きをフレームワーク側に委ねるための専用属性として、@UIApplicationMain@NSApplicationMain が提供されてきました。

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
    // ...
}

これらは便利ではあるものの、次のような問題がありました。

  • フレームワーク固有の属性がコンパイラに組み込まれている。UIKit と AppKit のためだけに専用の属性を用意しており、他のフレームワーク(例: コマンドラインツール用のライブラリや Web フレームワーク)は同様の仕組みを持てませんでした。
  • アプリ作者が「起動のための雛形コード」を書く必要がある場面が多い。専用属性のない領域では、main.swift を用意して SomeFramework.start() のようなブートコードを書くしかありませんでした。
  • 言語機能としての一般性がない。エントリポイントをフレームワーク側の型で提供するという設計は、本来であれば Swift の型システムの上で素直に表現できるはずのものでした。

フレームワーク非依存で、型に基づいてエントリポイントを指定できる軽量な仕組みが求められていました。

02 どのように解決されるのか

新しい属性 @main を導入し、「この型がプログラムのエントリポイントである」ことを宣言できるようにします。@main が付いた型は、static func main() というメソッドを一つ持つことだけが要求されます。プログラム開始時にはこの static メソッドが呼び出されます。

@main
struct MyProgram {
    static func main() {
        print("Hello, world!")
    }
}

これは概念的には、別途 main.swiftMyProgram.main() と書いたのと同じ動作になります。

フレームワーク側で main() を提供する

main() は通常の static メソッドなので、プロトコル拡張や基底クラスで提供できます。これによりフレームワークは、利用者に起動のための雛形を書かせることなく独自のエントリポイントを定義できます。

たとえば ArgumentParserParsableCommand プロトコルは、コマンドライン引数をパースして適切なコマンドを実行する main() を拡張で提供しています。利用者はそれに適合する型に @main を付けるだけで済みます。

@main
struct Math: ParsableCommand {
    @Argument(help: "A group of integers to operate on.")
    var values: [Int]

    func run() throws {
        let result = values.reduce(0, +)
        print(result)
    }
}

UIKit や AppKit も、UIApplicationDelegate / NSApplicationDelegatestatic func main() を追加することで、@UIApplicationMain / @NSApplicationMain の代わりに @main を使えるようにできます(この置き換え自体は本 Proposal のスコープ外です)。

エントリポイントの一意性

一つのプログラムのエントリポイントは一つだけでなければなりません。コンパイラは次のいずれか一つだけが存在することを保証します。

  • @main / @UIApplicationMain / @NSApplicationMain が付いた非ジェネリックな型
  • main.swift ファイル

main.swift は中身が空であっても常にエントリポイントとみなされるため、@main を付けた型を main.swift に書くことはエラーになります。

@main が使える場所

  • 型宣言にも、既存の型に対するエクステンションにも付けられます。
  • 対象の型は、アプリ本体のモジュールでも、インポートしてきたモジュールでも構いません。
  • クラス階層の基底クラスに付けることはできますが、継承はされません。エントリポイントになるのは @main を書いた型だけです。

main() の要件

main() は、以下のような要件を持つ仮想的なプロトコルに適合することと同等に扱われます。

protocol ProvidesMain {
    static func main() throws
}

したがって main() は、型自身で定義しても、スーパークラスから継承しても、適合先プロトコルのエクステンションで提供しても構いません。また、throws は付けても付けなくても構いません。main() から投げられたエラーは、トップレベルコードから投げられたエラーと同じ扱いになります。

SwiftPM との関係

提案時点では、SwiftPM は実行可能ターゲットを main.swift の有無で判定しているため、SwiftPM のパッケージ内で @main をそのまま使うことはできません(この点は別途 SwiftPM 側での対応が必要です)。

Future Directions

将来的には、main(Int, [String]) -> Int のように引数と終了コードを扱うシグネチャや、Windows の wWinMain のようなプラットフォーム固有のエントリポイントを @main 対象の型から提供できるようにすること、@main(console: false) のように属性に引数を渡せるようにすることなどが検討されています。いずれも今後の議論に委ねられており、本 Proposal のスコープ外です。