kateinoigakukunのブログ

思考垂れ流しObserver

GoodNotesでSwiftWasmの仕事を始めた

近況アップデート

5月の初旬からGoodNotes社で働いている。 GoodNotes社はiPad向けノートアプリGoodNotesを開発しているところ。

実は、去年の10月くらいからGoodNotesの人々がSwiftWasmプロジェクトにコントリビュートしてくれていた。 詳細は控えるが、GoodNotesアプリのコードベースをWebAssemblyにコンパイルして、クロスプラットフォームに移植することが目的。

コミュニティのDiscordでそこそこの頻度で社員と議論していたことで、お互いに顔見知りになり、声がかかったという流れ。

働くモチベーションとしては、

  • 自分が育てた技術を使ったプロダクトで価値を届けたい
  • (恐らく)現状自分の価値が最も高い環境でどの程度の評価をされるのか知りたい

の2つが大きい。

ロールとしては、プロダクト開発で発生した問題の相談に乗ったり、得られたフィードバックを元に実際に手を動かしてSwiftWasmツールチェインを改善したり、とコンサルタント的な感じ。 大規模なアプリ故のパフォーマンス問題や、製品レベルの品質を達成するための課題など、見えてこなかったポイントが見えてきたので楽しくやっていけそう。

オフィスはロンドンと香港にあるが、割とフルリモートで働いている人が多く、自分も日本からリモートで働いてる。 タイムゾーンを跨いでいるのでコミュニケーションは基本非同期だが、情報共有の工夫やドキュメンテーションの文化のおかげでなんとか働けている。

英語は周りに介護してもらってなんとか…という感じ。

まだ始まったばかりだが、ひとまずここ2週間くらいはチームメンバーからの信頼を得るためにあくせく働いている。

muslでビルドするにはmusl.ccが便利

Binaryenのビルド済みwasm-optを使うとセグフォする現象に遭遇した時、ビルドを手元で再現するためにmuslを使う必要があったのでメモ。1

musl libc

muslはlibcなので、コンパイラgccのままsysrootを差し替えることで大体うまいことコンパイルできる。

この設定をmusl-toolsパッケージのmusl-gccラッパーコマンドが勝手にやってくれるが、C++用のラッパーが用意されてなかったりスッとはビルドできない。あとツールチェインに入ってるSanitizer達がglibcを想定してビルドされてたり出来ないことが結構ある。

なので、最初からmuslをターゲットとしてビルドされたツールチェインが欲しくなり、そこでビルド済みのmuslツールチェインを配布しているmusl.ccが便利。2

$ curl -LO https://musl.cc/x86_64-linux-musl-native.tgz
$ tar xfz x86_64-linux-musl-native.tgz
$ tree -L 1 ./x86_64-linux-musl-native
./x86_64-linux-musl-native
|-- bin
|-- include
|-- lib
|-- libexec
|-- share
|-- usr -> .
`-- x86_64-linux-musl

CMakeで使う

musl.ccのツールチェインはsysrootをgccコマンドの相対で設定してくれるので、CMAKE_C_COMPILERさえ設定していれば、特に追加で指定する必要はない。

cmake ../.. -G Ninja \
  -DCMAKE_C_COMPILER=./x86_64-linux-musl-native/bin/gcc \
  -DCMAKE_CXX_COMPILER=./x86_64-linux-musl-native/bin/g++

  1. これは結局muslのスレッドスタックサイズのデフォルト値がglibcよりかなり小さくてスタックオーバーフローしてただけだった https://github.com/WebAssembly/binaryen/issues/4401

  2. これはOfficialではないがOpenJDKとかでも使われてる

Rubyのコミッタになりました

実は先日 ko1さんとmameさんから推薦をいただき、Rubyのコミッタになりました。

ここ数ヶ月間、Rubyアソシエーションの開発助成プロジェクトとしてCRubyのWASIサポートを進めており、WASIのプラットフォームメンテナが必要ということで。

WASI対応の話はPublickeyさんに良い感じにまとめていただきました。詳しい実装の話は後日書こうと思います。1

www.publickey1.jp

正直Rubyのことを聞かれても答えられる自信はありませんが、CRubyの実装はほんのり分かるようになりました。あとビルドスクリプトはどこもつらい。2

という訳で、晴れてSwiftとRubyのコミッタという謎の人材になりました。 引き続きどちらもやっていくので、よろしくおねがいします。


  1. 書きました An Update on WebAssembly/WASI Support in Ruby | by kateinoigakukun | ITNEXT

  2. SwiftはカスタムターゲットまみれのCMake、CRubyは4000行以上のconfigure.acが…

TypeProf for IDEの開発をお手伝いしました at クックパッド

TL;DR

インターンの内容

TypeProfはクックパッドでフルタイムRubyコミッタをされている@mametterさんが開発しているRubyの型プロファイラです。 Rubyのプログラムにできるだけ型注釈を入れずに抽象解釈によって型を推論する、という面白い特徴があります。

Ruby 3.0ではRBSのプロトタイプを生成するためのツールとしてRubyにバンドルされています。

github.com

今回のインターンでは、TypeProfの解析結果を利用したRubyのLanguage Serverの実装をお手伝いしました。

TypeProf for IDEについては今年のRubyKaigi Takeout 2021のKeynoteで発表があったので、雰囲気を知りたい方はこちらを見てください。

www.youtube.com

www.slideshare.net

実装

そもそもRubyを触るのが久々かつ、TypeProfがそこそこ大きいプログラムであったため、binding.irbデバッグ可能な環境を作るところからはじめました。

これを初手で準備したことでその後のコードリーディング効率が格段に上がりました。やはりデバッガは偉大。

Add --port option to lsp server for debugging purpose by kateinoigakukun · Pull Request #34 · ruby/typeprof · GitHub

その後mameさんに助けてもらいながら数種類のコードジャンプを実装しました。 ジャンプ先候補はTypeProfの解析結果を使っているので、解析器自体の実装も勉強できて良かったです。

もうひとつ面白い改善として、コード補完の高速化を行いました。

TypeProf for IDEではユーザが1タイプするごと (textDocument/didChange) にプログラムを解析し直すのですが、解析の仕組み的に一回の解析にかなり時間がかかります。 オリジナルの実装では解析中サーバスレッドをブロックしていたため、タイプするごとに解析待ちのリクエストが溜まっていました。

さらに、コード補完 (textDocument/completion)では変更後のプログラムを検証する解析とは別の解析がサーバスレッドで行われているため、1タイプごとに貯まる重いタスクが更に増え、若干もっさりしていました。

LSPではdidChangeは通知であり、すぐに結果を返却しなくても良く、更にコード補完リクエストもリクエストとレスポンスの順序を気にしないメソッドなので、高速化のために解析スレッドを分けることにしました。 また、新しい didChange 通知が来た時点で前の状態での解析結果は無効になるため、解析を途中で打ち切る機構を追加しました。

f:id:kateinoigaku:20210912110945p:plain

before after
typeprof-before typeprof-after

github.com

その他にも色々と実装できて楽しかったです。

インターン中のPRs

Ruby本体への貢献

TypeProf for IDEは最新の開発版MRI (Matz's Ruby Interpreter) のAPIを使っており、CIではデイリービルドされた最新のRubyバイナリを使っています。

具体的には、ruby/setup-rubyGitHub Actionsのアクション経由で ruby/ruby-dev-builder でビルドされた成果物を使っていました。

しかし、ある日直近で入ったAPI変更に追従する変更をTypeProfに入れたところ、CI上のテストが失敗し始めました。

調査したところ、ruby/ruby-dev-builderのデイリービルドがmacOS上で数十日間失敗しており、最新のバイナリが全くアップロードされていないことがわかりました。具体的には、macOSにデフォルトで同梱されているGNU Makeのバージョンが古いことが原因で、最新のruby/rubyMakefileを正しく解釈できていませんでした。

そして、特定のビルドオプションを付けたときのみ再現する問題であったため、ruby本体のCIを奇跡的に通り抜けていました。

macOSはGPLv3を避けるためにGNU Makeのバージョンを3.81で止めている。ちなみに3.81は2006年リリース。最新は4.2

とりあえずの対処としてGNU Make 3.81で最新のバージョンと同様の動作をさせるためのワークアラウンドを追加しました。

github.com

ということで晴れてRuby Contributorの実績も解除できました。

クックパッドの環境

ruby-devチームの朝会では@_ko1 さんと@mametterさんがRubyの話を、僕がSwiftとWebAssemblyの話をする機会があったり、母国語で言語処理系のプロと働けるとても貴重な環境でした。

感想

Ruby自体の経験は浅かったものの、言語処理系や巨大プロジェクト開発の経験のお陰で、ある程度の貢献ができたかなと思います。

また、Ruby開発には単純に人手が足りず、自分でも貢献できそうなインパクトの大きいタスクが残っていたり、出来ることはたくさんありそうなので、時間を見つけて継続して関わっていけたらなと思います。

今回のインターン中関わってくださった皆さんありがとうございました。

SwiftコンパイラのAuto-linkingとそれを直した話

前回のエントリでAuto-linkingについて解説しました。今回はSwiftコンパイラにおけるAuto-linkingの使われ方と、最近それを直した話をします。

kateinoigakukun.hatenablog.com

用語定義

  • モジュール: Swiftのimportできる単位。 .swiftmodule.swiftinterfaceまたは module.modulemap が実態。Cで言うヘッダ
  • ライブラリ: libfoo.a とか libfoo.dylib。大抵モジュールと1対1になってる。

SwiftのAuto-linking

C言語では以下のようなpragmaを書くことでリンクするライブラリを指定していました。

#include <math.h>

#pragma comment(lib, "m")

一方で、Swiftでは import 文を書くだけで、インポートしたモジュールに紐づくライブラリがリンクされます。 XcodeiOSアプリを書く際、明示的にUIKitのリンク設定をすることなくビルドが成功しているのは、Auto-linkingの仕組みのおかげです。

import UIKit

モジュールにライブラリを紐付ける為には、モジュールをビルドする際に -module-link-name オプションを付ける必要があります。このオプションを明示的に指定しない場合Auto-linkingは有効になりません。

// foo.swift
public func inc(_ v: Int) -> Int { v + 1 }

// main.swift
import foo
_ = inc(1)
# モジュール "foo" をコンパイル
$ swiftc -emit-module -emit-library foo.swift -module-link-name foo
$ ls
foo.swift
foo.swiftmodule
libfoo.dylib
foo.swiftdoc
foo.swiftsourceinfo

# -lfoo 無しで動く
$ swiftc main.swift -I. -L.

llvm-bcanalyzer -dump foo.swiftmodule でモジュールファイルの中身を眺めると、 LINK_LIBRARY"foo" が指定されていることがわかると思います。 コンパイラはimport文で読み込んだモジュールファイルからLINK_LIBRARYエントリを探し、得られたライブラリの情報をオブジェクトファイルに格納します。

// foo.swiftmodule
<MODULE_BLOCK NumWords=610 BlockCodeSize=2>
  <INPUT_BLOCK NumWords=23 BlockCodeSize=4>
    <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'Swift'
    <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'SwiftOnoneSupport'
    <LINK_LIBRARY abbrevid=6 op0=0 op1=0/> blob data = 'foo'
  </INPUT_BLOCK>
</MODULE_BLOCK>

以下にmacOS上でのMach-Oオブジェクトファイルの内容を示します。前回のエントリで紹介した LC_LINKER_OPTION-lfoo が埋め込まれていますね。これをリンカに渡すことでユーザはリンカオプションを渡すこと無く、ライブラリをリンクできます。実は標準ライブラリも同様の仕組みでリンクされています。

$ swiftc -c main.swift -I. -o main.o
$ llvm-objdump -m -x main.o
...
Load command 4
     cmd LC_LINKER_OPTION
 cmdsize 24
   count 1
  string #1 -lfoo
Load command 5
     cmd LC_LINKER_OPTION
 cmdsize 40
   count 1
  string #1 -lswiftSwiftOnoneSupport
Load command 6
     cmd LC_LINKER_OPTION
 cmdsize 24
   count 1
  string #1 -lswiftCore

間接依存するライブラリのAuto-linking

さて、モジュールとライブラリの依存関係が次のようになっていた場合を考えてみましょう。 mainはbarだけをインポートしていて、barを介してfooに間接依存していますね。

この場合、最終的な成果物にはfooとbar両方がリンクされていて欲しいです。

main -> bar -> foo
// foo.swift
public func inc(_ v: Int) -> Int { v + 1 }

// bar.swift
import foo
public func dec(_ v: Int) -> Int { inc(v) - 2 }

// main.swift
import bar
_ = dec(1)

コンパイラmain.swift を処理する中で import barから、bar.swiftmoduleを読み込みます。 直接依存するbar.swiftmoduleには ライブラリbarをリンクするLINK_LIBRARYエントリと、barモジュールがfooモジュールに依存していることを示す IMPORTED_MODULE エントリが含まれています。 コンパイラIMPORTED_MODULE によるモジュールの依存グラフを辿っていき、全てのモジュールから LINK_LIBRARY をかき集めます。

// bar.swiftmodule
<MODULE_BLOCK NumWords=612 BlockCodeSize=2>
  <INPUT_BLOCK NumWords=25 BlockCodeSize=4>
    <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'Swift'
    <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'SwiftOnoneSupport'
    <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'foo'
    <LINK_LIBRARY abbrevid=6 op0=0 op1=0/> blob data = 'bar'
  </INPUT_BLOCK>
</MODULE_BLOCK>

最終的にオブジェクトファイルには期待通りfooとbar両方のリンカオプションが埋め込まれます。

Load command 4
     cmd LC_LINKER_OPTION
 cmdsize 24
   count 1
  string #1 -lbar
Load command 5
     cmd LC_LINKER_OPTION
 cmdsize 24
   count 1
  string #1 -lfoo

プライベートな依存を表現する @_implementaionOnly

Swiftにはimport文にいくつかのバリエーションがあり、その内の1つに@_implementaionOnlyというアトリビュートがあります。

セマンティクスは「importしたモジュールの型が自身のpublicなAPI/ABIに露出していないことを保証する」です。 言い換えると、「API/ABIは再エクスポートせず実装のみをライブラリ内で使う」になると思います。

これにより、ライブラリ作成者は自身の依存をプライベートにでき、ライブラリのABIを壊さずに依存ライブラリを変更出来るようになります。

詳しい議論については以下のフォーラム投稿を見てください。

forums.swift.org

ここで、次のケースを考えます。モジュールfooに依存するモジュールbarはfooをプライベートにインポートします。 依存をプライベートにする、ということはライブラリの利用者mainは間接依存するfooの存在について関知せず、mainのリンク時にfooをリンクする必要があるか分かりません。 たとえば、barが共有ライブラリとして配布されている場合、そこにfooが静的にリンクされている場合もあります。その場合mainをリンクする際には -lbar だけあれば良いです。むしろ libfoo.a が配布されているとは限らないため、 -lfooをmainのリンク時に渡すと、ライブラリfooが見つからずリンクエラーになるかもしれません。

main -> bar -> foo
// foo.swift
public func inc(_ v: Int) -> Int { v + 1 }

// bar.swift
@_implementaionOnly import foo
public func dec(_ v: Int) -> Int { inc(v) - 2 }

// main.swift
import bar
_ = dec(1)

ということで、 @_implementaionOnly 付きでインポートされると、その先のモジュール依存グラフを追わなくなります。 つまり、barをインポートする環境にfoo.swiftmodule が配布されていなくても良いわけです。

@_implementaionOnlyの有無によるbar.swiftmoduleの差分を以下に示します。なにやら IMPORTED_MODULE エントリにフラグが立っていますね。 これが@_implementaionOnlyの印です。

このbarをインポートしたmain.swiftをコンパイルすると、-lbar だけが埋め込まれたオブジェクトファイルができます。fooへの依存が隠蔽されてますね。

// bar.swiftmodule
 <MODULE_BLOCK NumWords=612 BlockCodeSize=2>
   <INPUT_BLOCK NumWords=25 BlockCodeSize=4>
     <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'Swift'
     <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'SwiftOnoneSupport'
-    <IMPORTED_MODULE abbrevid=4 op0=0 op1=0 op2=0/> blob data = 'foo'
+    <IMPORTED_MODULE abbrevid=4 op0=2 op1=0 op2=0/> blob data = 'foo'
     <LINK_LIBRARY abbrevid=6 op0=0 op1=0/> blob data = 'bar'
   </INPUT_BLOCK>
 </MODULE_BLOCK>

@_implementaionOnly によって壊れた静的Auto-linking

さて、@_implementaionOnly 付きでもAuto-linkingはうまく動いているように思われましたが、実はこれだけでは上手く行かないケースがあります。

ここでは実際に壊れてしまった Foundation (apple/swift-corelibs-foundation)のケースを紹介します。 Foundationのモジュール依存関係は以下のようになっています。(説明のため簡略化してます)

Foundation -> CoreFoundation

さらに、CoreFoundationモジュールは module.modulemapで定義されており、依存ライブラリとして icui18n を指定しています。 そのためライブラリの依存関係としては、次のようになっています。

Foundation -> CoreFoundation -> icui18n

CoreFoundationはツールチェインユーザーから直接使われることを想定しておらず、Foundationのプライベートな依存であるため、ある日全ての import CoreFoundation@_implementaionOnly 付きに変更されました。これにより、ツールチェインにCoreFoundationのヘッダやmodulemapを含める必要が無くなります。(実際はまだ含まれていますが究極的には必要ないです。)

しかし、よく考えてみてください。「Foundationのプライベートな依存CoreFoundation」が依存するICUのicui18nは本当にプライベートな依存ライブラリでしょうか? icui18nはSwift標準ライブラリでも使われており、プログラム全体で共有すべき依存です。 しかし、Foundationはicui18nへの直接のライブラリ依存を持っておらず、CoreFoundationへの依存がプライベートとなってしまった状態ではリンカオプションを伝播する手段がありません。

この問題により、Swift 5.4のLinux向けツールチェインではFoundationの静的リンク時にシンボル不足エラーが出ています。

[SR-14536] Importing FoundationNetworking with -static-stdlib is broken with missing symbols on Linux - Swift

修正編

この問題を解消するためには、Foundationにicui18nへのライブラリ依存(=LINK_LIBRARYエントリ)を持たせれば良いわけです。 しかしSwift 5.4当時のコンパイラに任意のライブラリの LINK_LIBRARY エントリをモジュールに差し込む機能はありませんでした。

そこで、Swift Forumで解決策について議論した上で、コンパイラに新しいオプションを追加実装しました。

このパッチでは、-public-autolink-library というオプションを追加して、指定したライブラリを LINK_LIBRARYエントリ に出力するようにしています。

forums.swift.org

github.com

コンパイラ側のパッチがマージされると、ようやくFoundation側の修正に取り組めます。 このパッチでは、追加したオプションを使って icui18n へのライブラリ依存をFoundationに足しています。

github.com

その後リバートやリバート返しを経て、最近ようやく直りました。

まとめ

Auto-linkingを維持するために多大な努力と時間が費やされていることが伝われば嬉しいです。 結局最初に問題に気がついてから修正まで半年くらいかかってます。大半はコミュニケーションですが。

すごく頑張ったのでLinuxでSwiftを静的リンクしてる人たちが救われてほしい。

Auto-linkingまとめ

Auto-linkingとは

Auto-linkingとは、コンパイラが出力したオブジェクトファイルからリンク対象のライブラリを自動的に決定する仕組みです。 通常、ユーザはリンカのコマンドライン引数に -lm のようにリンクするライブラリを指定しますが、Auto-linkingをサポートするコンパイラとリンカを使う場合、以下のようなC言語#pragma コメントを記述することで、リンカオプションを指定をせずに、リンカにライブラリを伝えることができます。

#include <math.h>
#include <stdio.h>

#pragma comment(lib, "m")

int main(void) {
  printf("PI = %f\n", atan(1) * 4);
  return 0;
}

コンパイルの様子

# -lmの指定無しでリンクが成功する
$ clang main.c -fuse-ld=lld
$ ./a.out
PI = 3.141593

Auto-linking - Wikipedia

リンカオプションを指定する必要が無くなることで、ユーザの負担が減るだけでなく、ビルドシステム中のオプション伝播が単純化されます。嬉しいですね。

この記事ではオブジェクトファイル形式ごとのAuto-linkingの実装状況と、LLVMにおけるサポートについて解説します。

(完全に理解したがしばらくして忘れて調べ直す、を何回かやったのでいい加減文字に起こすことにした)

オブジェクトファイル形式ごとの仕組み

上で紹介した #pragma comment(lib, "...")IBMコンパイラ、MSVC、Clangが提供しているディレクティブです。(他にもサポートしてるコンパイラはあるかも) MSVCはCOFF、ClangはELF、Mach-O、COFF向けにこの機能を実装しています。

COFF

MSVCが出力するオブジェクトファイル形式COFFには、リンカオプションを格納する.drectve セクションがあります。このセクションはオブジェクトファイルのみでサポートされており、通常のセクションとは異なり、最終的な実行可能ファイルには含まれません。

コンパイラ#pragma comment(lib, "...") からリンカオプションを生成し、オブジェクトファイルの.drectveセクションに埋め込むことで、link.exeコマンドにリンクするライブラリを伝えています。

PE Format - Win32 apps | Microsoft Docs: The .drectve Section (Object Only)

ELF

ELFにはCOFFのような仕様として定義されたリンカオプション用セクションはありません。 その代わりに、LLVMがClangとlldの間のコンベンションとして特殊なセクションを定義しています。 そのため、Clangで#pragma comment(lib, "...")コンパイルしたとしても、goldやGNU ldでリンクする場合オプションが伝わりません。

LLVMの実際の実装については後述します。

Mach-O

Mach-Oオブジェクトファイルには、ヘッダの後ろにLoad Command呼ばれる構造体列が配置されており、セクションやセグメントなどのレイアウト、動作に必要なOSバージョンなど、リンカやプログラムローダに伝える情報が格納されています。このLoad Commandの一つに、 LC_LINKER_OPTION があり、その名の通りリンカオプションをリンカに伝えてくれます。

このあたりの情報は明確なドキュメントを見つけられておらず、実装を追って分かったことなので、Mach-Oの規約として定義されているものなのかは分かりません。(もしドキュメントの在り処をご存じの方がいれば教えて下さい)

リンカ(ld64)側の実装はこのあたりにあります。 macho_relocatable_file.cpp

LLVMにおけるAuto-linking

さて、LLVMにはAuto-linkingを実現するための機能が歴史的経緯により2つあります。どちらもLLVM IR上のモジュールメタデータとして表現されており、コンパイラLLVM IRを生成する際に指定します。

1つ目は、llvm.linker.options です。

このメタデータは、COFFでは.drectveMach-OではLC_LINKER_OPTIONに降下します。 一応ELFでも SHT_LLVM_LINKER_OPTIONS という特別なセクションにリンカオプションが埋め込まれるようにコード生成されますが、なんと肝心のlld側が対応していません。 他のオブジェクトファイルの形式と違い、任意のリンカオプションを受け付けるのではなく、オプションのセマンティクスを明示的にしたKey-Valueペアで表現するため、 コンパイラフロントエンドがELF向けに特別な処理を入れる必要があります。

SwiftのWindowsポートで有名なcompnerdさんが2018年に提案して、ClangフロントエンドとLLVMバックエンドの実装をしましたが、その後アップデートが無いようです。

2つ目は llvm.dependent-librariesです。

llvm.linker.optionsのELFサポートが難航している状態に対して、SonyのPlay Station向けツールチェイン開発者の方が新しく導入したメタデータです。 これは、ELFのみをサポートしており、 リンクするライブラリ名の配列を受け付け、オブジェクトファイルのSHT_LLVM_DEPENDENT_LIBRARIES というセクションに埋め込みます。 こちらはClang、LLVMバックエンド、リンカの実装が完了しています。現在のClangは#pragma comment(lib, "...")をELFの場合のみ、llvm.linker.optionsを使う実装になっています。

まとめ

大変

参考図書

ISUCON11予選でSwift移植チャレンジした

TR;DR

  • ISUCON11予選にSwiftで参加した
  • SwiftのConcurrency機能(async/await等 )の体験最高
  • 素振りが足りず地区予選敗退

チーム情報

移植の覚悟

ISUCONでは例年、いくつかの言語で対象Webアプリケーションの参考実装が提供されます。しかし、それを使わないといけない、というわけではなくレギュレーション上は他言語の使用が許可されています。

許可される事項には、例として以下のような作業が含まれる。

・ ...

・他の言語による再実装

ISUCON11 予選レギュレーション : ISUCON公式Blog

Swiftの実装は残念ながら提供されていませんが、せっかく参加するのであればチームの得意分野を活かしたい、ということで競技時間内にSwiftで再実装する事になりました。

採用技術

ツールチェイン

通常、iOSmacOS向けのSwiftアプリを開発する場合、デプロイ先のOSバージョンに同梱されているSwiftランタイムバージョンによって使えるSwiftの言語バージョンが制限されてしまいます。

が、今回はLinuxにデプロイするSwiftアプリを開発するため、特に制約はありません。最新の言語機能も使い放題です。

というわけで、Swiftツールチェインとして正式リリース前のSwift 5.5を採用しました。Swift 5.5には並行プログラミングの言語機能が追加されており、Non-blocking IOを使ったアプリを書くのにもってこいです。

今回はswift-5.5-DEVELOPMENT-SNAPSHOT-2021-07-30-aを使いました。

Webフレームワーク

現在アクティブにメンテナンスされているSwift製Webフレームワークはかなり少なくなっており、ほぼVapor一択な状況でした。 かなり重厚な作りになっていますが、必要な機能は一通り揃っています。

github.com

Vaporが依存しているNon-blocking IOを実現するライブラリSwiftNIOは、言語側のConcurrency機能との橋渡しをサポートしており、SwiftNIO内のFuture型を簡単にasyncメソッド呼び出しとして取り出すことができます。

github.com

また、VaporはSwiftNIOのConcurrencyサポートを利用して、Vapor自体のConcurrency APIサポートを進めており、 メインメンテナの@0xTimさんのPR上で開発が進行している状況です。今回はこのPRブランチを使用しました。

github.com

SQLライブラリ

Vaporは複数のSQLバックエンドをサポートするための抽象レイヤとしてFluent というORMを提供しています。しかし過去の経験から、抽象化されたDSLで思い通りのSQLを発行するのが非常に難しいことが分かっていたため、今回は採用を見送りました。

代わりに、バックエンドをMySQLのみに絞ったMySQLKitを採用しました。(FluentでMySQLバックエンドを使う場合もこれが使われる)

MySQLKitはlibmysqlclientに依存せず、SwiftNIOベースの非同期IOで実装されたMySQLNIOMySQLクライアントとして使っています。ちょっとうれしいですね。

MySQLKitは高度な機能は提供しませんが、行をSwiftの構造体にマップするできるので今回の用途では十分でした。

struct Response: Decodable {
  var character: String
}
let characters = try await pools.withConnection { conn in
  try await conn.sql()
    .raw("SELECT `character` FROM `isu` GROUP BY `character`")
    .all(decoding: Row.self).get()
}

github.com

最終的なPackage.swift

SwiftNIOがConcurrency APIにOSバージョンベースのavailabilityを付けちゃってるので、macOS 12未満のmacOS上で開発するために disable-availability-checking を付けているのがミソ

// swift-tools-version:5.3
import PackageDescription

let package = Package(
    name: "isucon11",
    platforms: [
       .macOS(.v10_15)
    ],
    dependencies: [
        .package(url: "https://github.com/vapor/vapor.git", .branch("async-await")),
        .package(url: "https://github.com/vapor/mysql-kit.git", from: "4.0.0"),
        .package(url: "https://github.com/Flight-School/AnyCodable", from: "0.6.0"),
        .package(url: "https://github.com/vapor/jwt-kit.git", from: "4.0.0"),
    ],
    targets: [
        .target(
            name: "App",
            dependencies: [
                .product(name: "Vapor", package: "vapor"),
                .product(name: "MySQLKit", package: "mysql-kit"),
                .product(name: "JWTKit", package: "jwt-kit"),
                .product(name: "AnyCodable", package: "AnyCodable")
            ],
            swiftSettings: [
                .unsafeFlags(["-cross-module-optimization"], .when(configuration: .release)),
                // Disable availability checking to use concurrency API on macOS for development purpose
                // SwiftNIO exposes concurrency API with availability for deployment environment,
                // but in our use case, the deployment target is Linux, and we only use macOS while development,
                // so it's always safe to disable the checking in this situation.
                .unsafeFlags(["-Xfrontend", "-disable-availability-checking"])
            ]
        ),
        .target(name: "Run", dependencies: [.target(name: "App")])
    ]
)

過去問を解く

過去問としてISUCON 10の予選問題を解きました。素振りによってVaporやMySQLKitに慣れたり、Vaporのデフォルト設定の罠が把握できたのでかなり良かったです。

ただ、もう1年分くらい解いておけば本番でハマった問題も回避できたかなぁという気持ちです。過去問をやろう。

3人で13時間くらい使って移植した結果、他言語の初期実装と同程度のスコアを出すことには成功していました。

本番結果

練習の成果やasync/awaitの開発体験の良さもあり、4時間程度でおおよその実装が完了していましたが、その後のベンチマーカ互換性チェックのデバッグに手間取り、最終的にチェックを通過することができずスコアは0でした。

主なハマりポイントはDB内でのタイムゾーンの扱いでした。 MySQLNIOはDATETIME型のマッピングの際のタイムゾーン考慮はユーザの責務であるとし、ライブラリ内では常にGMTとして扱うため、9時間のズレにかなり悩まされました。

時間はむずかしい。

感想

かなり楽しかった。

  • 愛と気合で移植できる
  • 癖を把握する必要はあるがSwiftでもイケる
  • 他の言語実装を動かしながら、一部のリクエストだけSwift実装に流す、みたなことができていればスコア0は回避できたかも。
  • 来年再挑戦したい