fatalErrorから復帰させる不思議なSwiftライブラリFortifyを読み解く

Swiftにおいて、`fatalError()` や `preconditionFailure()`,  Optionalの`!`や`try!`などで発生したエラー、すなわちSwiftのエラーの分類で言うところの**Universal error**, **Logic failure**は基本的には発生したその時点でプログラムが終了してしまいます。

```swift
guard i > 0 else {
	fatalError("error") // ここでプログラムは落ちる
}
```


 参考: [Swiftのエラー4分類が素晴らしすぎるのでみんなに知ってほしい - Qiita](https://qiita.com/koher/items/a7a12e7e18d2bb7d8c77)

しかし先日Twitterで[Fortify](https://github.com/johnno1962/Fortify)というライブラリを見かけて、Swiftを使い始めて以来最大の衝撃を受けました。Fortifyはたった150行の1つのSwiftファイルからなるシンプルなライブラリですが、なんとUniversal error, Logic failureから復帰する機能を提供しているのです！

```swift
// FortifyのREADMEより

import Fortify

do {
    try Fortify.exec {
        var a: String!
        a = a! // 普通はここで落ちるはず
    }
}
catch {
    // が、catchできるErrorに変換されてここで拾える
    NSLog("Unwrap error: \(error)")
}
```

この記事ではFortifyがいかにしてこの挙動を実現しているのかを解説してみたいと思います。

## 落ちるコードから復帰するには？
この挙動を実現するためには、ざっくり2つの壁を乗り越える必要がありそうです。

1. プログラムが落ちたことをプログラムから拾う
2. どこか動くところに戻る

もちろんSwift言語自体にはそんなトリッキーな制御フローを実現する機能はありません。なので、**OSやC言語の力を借りてこれらをなんとか実現していきます**。

## fatalErrorを呼んだら何が起こる？
まずは`fatalError()` を例に落ちた時になにが起こるかをみてみます。環境はmacOS / x86_64 / Swift 4.1です。

```fatal.swift
fatalError()
```

```
$ swiftc fatal.swift
$ lldb -- ./fatal
```

`libswiftCore.dylib` の中で、`ud2`という命令で止まっています。

```
(lldb) run
(lldb) c
...
->  0x10027fd00 <+144>: ud2
```

Intelのマニュアルによると、`ud2`によりInvalid opecode exception(無効オペコード例外) が発生します。

> Executed a UD2 instruction. Note that even though it is the execution of the UD2 instruction that causes the invalid opcode exception, the saved instruction pointer will still points at the UD2 instruction.

通常OSはこのような例外をカーネルで処理できない場合には、元のプロセスに**シグナル**を送ります。macOSでは、Invalid opecode exceptionを**SIGILL**としてプロセスに送るようです。

見るべきところがあっているかはちょっと怪しいですが、[ここ](https://github.com/apple/darwin-xnu/blob/0a798f6738bc1db01281fc08ae024145e84df927/osfmk/kdp/ml/x86_64/kdp_machdep.c#L405-L408)とか[ここ](https://github.com/apple/darwin-xnu/blob/5394bb038891708cd4ba748da79b90a33b19f82e/bsd/uxkern/ux_exception.c#L450-L452) にそれっぽいコードがあります。

```c
case T_INVALID_OPCODE:
	exception = EXC_BAD_INSTRUCTION;
```

```c
case EXC_BAD_INSTRUCTION:
	 *ux_signal = SIGILL;
```

ここでは`fatalError`を例に見ましたが、だいたい他のクラッシュする例も同じく`ud2`からの`SIGILL`が発生します。確認していませんが、Linuxカーネルでも同じはずです。

## シグナル / シグナルハンドラ
カーネルからプロセスに送られてきたシグナルは、**シグナルハンドラ**を使ってプログラムで拾うことができます。これで1つ目の「プログラムが落ちたことをプログラムから拾う」を達成できそうです！

Fotifyの実装をのぞいてみましょう。`Fortify.exec` を呼び出すと、中でシグナルハンドラが設定されます。上で確認した`SIGILL`の他に、`SIGABRT`に対してもハンドラを設定しています。

```swift
public static let installHandlerOnce: Void = {
    _ = signal(SIGILL, { (signo: Int32) in
        Fortify.escape(msg: "Signal \(signo)")
    })
    _ = signal(SIGABRT, { (signo: Int32) in
        Fortify.escape(msg: "Signal \(signo)")
    })

    disableExclusivityChecking()
}()
```


`signal`はCで提供されているシグナルハンドラを設定するための関数ですが、ClangImporterによりSwiftのコードからも呼び出すことができます

実際にSwiftから見える定義はこんな感じです。

```swift
public func signal(_: Int32, _: (@convention(c) (Int32) -> Swift.Void)!) -> (@convention(c) (Int32) -> Swift.Void)!
```

余談ですが、`@convention(c)`が付いているので`self`などをキャプチャできないため、classメソッドとして`Fortify.escape`等を定義しています。

## setjmp / longjmp

さて、シグナルを拾うことでまた自分のプロセスに処理が戻ってきましたが、どうやってそこから元の正常な処理に戻るのでしょうか？

実はCで提供されている関数に**setjmp** / **longjmp**という恐ろしい(？) ペアが存在します。
簡単にいうと `setjmp`を呼んだあとで`longjmp`を呼べば、最初に`setjmp`を呼んだ位置に**無理やり戻れる**という代物です。

+ 参考: [いにしえのsetjmp()とlongjmp()、あるいはCの例外とは - Qiita](https://qiita.com/jj1bdx/items/ba1b71641cd51a5ac186)

これによって「どこか動くところに戻る」ことができるのです！
実際にFortifyの実装を見てみましょう。

`Fortify.exec` の中で `setjump`を呼び出しています。最初に呼び出したときには0が返るのでifの中には入らず、`exec`の引数に渡されたクロージャを実行します。

```swift
if setjump(stack! + (local.stack.count-1)) != 0 {
    throw local.error ?? NSError(domain: "Error not available", code: -1, userInfo: nil)
}

return try block()
```

先程シグナルハンドラの中で呼び出していた`Fortify.exec`の中で`longjump`を呼び出しています。2番目の引数に1を渡していて、これが`setjump`の場所に戻った際の返り値になります。

```swift
open class func escape(withError error: Error) -> Never {
...
    let stack = local.stack.withUnsafeMutableBufferPointer { $0.baseAddress }
    longjump(stack! + (local.stack.count-1), 1)
}
```

そうすれば今度は`if`の中に入りthrowされる、というわけです。
これで目的達成です！

## `@_silgen_name`でシンボル名を指定してClangImporterの制限を回避する
Cで定義されているのは`setjmp` / `longjmp` ですが、上で見たSwiftのコードに出てきた関数名は `setjump` / `longjump`でした。
`signal`と同じように、ClangImporterの働きによって`setjmp`や`longjmp`も使えそうなものですが、実装を見てみるとわざわざSwift側にこんな宣言をおいて名前をつけなおしています。なんでこんなことをしているのでしょうか？

```swift
@_silgen_name ("setjmp")
public func setjump(_: UnsafeMutablePointer<jmp_buf>!) -> Int32

@_silgen_name ("longjmp")
public func longjump(_: UnsafeMutablePointer<jmp_buf>!, _: Int32) -> Never
```

実は一部のCの関数はSwiftでは安全に扱えない可能性があるために、使おうとすると[ClangImporterがエラーにしてしまいます](https://github.com/apple/swift/blob/a264b1ed9c979ea61cb8bf466c1b087167ccf025/lib/ClangImporter/ImportDecl.cpp#L3625-L3636)。
`setjmp` / `longjmp`もその対象で、有名なところだと`fork`なんかもSwiftからは呼び出せません。

```cpp
if (decl->hasAttr<clang::ReturnsTwiceAttr>()) {
    // The Clang 'returns_twice' attribute is used for functions like
    // 'vfork' or 'setjmp'. Because these functions may return control flow
    // of a Swift program to an arbitrary point, Swift's guarantees of
    // definitive initialization of variables cannot be upheld. As a result,
    // functions like these cannot be used in Swift.
    Impl.markUnavailable(
      result,
      "Functions that may return more than one time (annotated with the "
      "'returns_twice' attribute) are unavailable in Swift");
}
```

これを回避するためにわざわざ別名を付けておいて、`@_silgen_name`で本当の名前を指定することでうまくリンクされて使える、みたいな遠回りをしています。

## その他の参考になるテクニック

150行のコードの中にそのほかにも「Swiftからでもこんなことできるのか….」と思わせられるいろいろなテクニックが使われています。例えば

+ `dlopen` / `dlsym`を使ってSwift Runtimeのフラグを書き換えてLoEをオフにする。
+ `pthread`を使ってスレッドローカルなオブジェクト作成

などなどです。詳細は是非読んでみてください。


## Fortifyの使い所

なかなかトリッキーなことをやっているのでiOSアプリで使うのは厳しいとは思いますが、それでも使い所はあると思っています。

１つはREADMEや[紹介記事](http://johnholdsworth.com/fortify.html)にかかれている通りサーバーサイドSwiftで使う、もう一つは**テストで使う**です。

`guard`の`else`の中で`fatalError`で落とすようなパターンは非常によく使われますが、テストをしようとするとその時点でクラッシュしてしまうので、正常系しかユニットテストを書くことができません。Rustの`#[should_panic]`ように言語レベルでサポートがあればよいのですが、残念ながらSwiftにはありません。。

**しかしFortifyを使えばそのようなケースもテストできます！**

実際にこんなライブラリを作ってみたので良かったら使ってみてください。

[GitHub - ukitaka/XCTAssertUnrecoverable: Make it possible to test that universal error / logic failure occurs.](https://github.com/ukitaka/XCTAssertUnrecoverable)

<img width="922" alt="image.png" src="https://qiita-image-store.s3.amazonaws.com/0/14846/2bda258f-bc11-368a-a196-a49f044c18a0.png">

<img width="966" alt="fail.png" src="https://qiita-image-store.s3.amazonaws.com/0/14846/fa4b04f0-cd0c-829b-2737-037cffcd8772.png">




## おわりに

たった150行のSwiftライブラリですが、本当に多くのテクニックが詰まっていてとても勉強になりました。
Swift4.0まではSwift Runtimeにパッチを当てないと使えませんでしたが、Swift4.1からはパッチ無しで動かせるようになったようなのでみなさんもぜひ使ってみてください！

<blockquote class="twitter-tweet" data-lang="ja"><p lang="en" dir="ltr">“Fortify&quot; converts fatal <a href="https://twitter.com/hashtag/swiftlang?src=hash&amp;ref_src=twsrc%5Etfw">#swiftlang</a> unwrap and casting errors into catchable exceptions for use in server apps: <a href="https://t.co/FiBNVm477U">https://t.co/FiBNVm477U</a> - now works with standard Swift 4.1 release.</p>&mdash; Injection for Xcode (@Injection4Xcode) <a href="https://twitter.com/Injection4Xcode/status/989581469675872257?ref_src=twsrc%5Etfw">2018年4月26日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>