kateinoigakukunのブログ

思考垂れ流しObserver

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は回避できたかも。
  • 来年再挑戦したい

2020年のオープンソース活動

2020年はOSS開発に多く時間を使ってきたなぁと思ったのと、GitHub Sponsorsからの振り込みがあったので、報告を兼ねて軽く今年何をしてきたか振り返ってみようと思います。

SwiftWasm

年間を通してずっと開発していたプロジェクトの一つです。このプロジェクト関連でコンパイラやリンカ、デバッガなどいろいろなものを作ってきました。 必要に迫られてLLVMICUなどの有名どころのプロジェクトにもコントリビューションする機会がありました。

ツールチェーンがおおよそ動くようになってからは、コントリビュータを増やすための発信をしたりディスカッションの場を用意したり、といった活動もしています。 最近は来るSwift 6に導入予定のasync/awaitのWasm上でのプロトタイプを実装しています。

進捗報告

つくったもの

GSoC

4月の時点でSwiftWasmはおおよそ動くようになっていたので、あとはバイナリサイズの縮小のための最適化をやりたいなと思っていたところ、運の良いことにちょうど良いプロジェクトがあったので応募してみました。

実際には5月から8月までGoogle Summer of Codeの学生としてSwiftのリンク時最適化を実装していました。詳しい話はSwiftコンパイラの勉強会「わいわいswiftc」で話したので興味がある方はスライドか動画をみてください。

実証実験としてある程度の成果はあったもののメインストリームにはまだ入っていません。2021年内には入れたいです。

f:id:kateinoigaku:20201231230651p:plain

IBLinter

かれこれ作ってから3年目になりました。SwiftUIが登場してお役御免になるかと思いきや、まだ暫くは使っていただけている様子で、ちょこちょこ新しい機能が追加されています。 今年面白かったのは@r_plusさんのPRで実装されたUIStackViewのbackgroundColor検知機能です。

Periphery

使われていないコードを検出してくれるツールなのですが、暫く開発が止まっており最新のXcodeで動かなくなっていたのでPRを投げていたところメンテナになりました。その後はSourceKitベースの解析をIndexStoreベースの解析に切り替えたり、Xcodeのアップデートに追従したりといったコントリビューションをしていました。最近オリジナル作者の @ileitchさんが開発を再開していますが、スター数の割にコントリビュータが少ないのが少々不安です。興味がある方はぜひ。

まとめ

ここに書いたほぼすべての活動は、インターン先であるメルペイの業務時間でコミュニティ活動として行われています。会社の支援がなければ学業と両立してここまでやってこれていないので本当に良い社だと思います。

また、ありがたいことにGitHub Sponsorsで支援してくださる方もいらっしゃり、活動のモチベーションの維持に大きくプラスになっています。いつもありがとうございます。

来年も継続して活動していけるよう頑張っていきます。

Swiftに搭載予定のC++相互運用機能

Swift Advent Calendar 2020 の1日目の記事です。

今年の頭頃からGoogleのSwift for Tensorflowチームが主体となって、SwiftとC++の相互運用機能の議論と実装が進んでいます。

まだ未実装な部分も多く今すぐに試せるわけではありませんが、マニフェストの内容や現在の実装状況から興味深い部分をピックアップして紹介したいと思います。

github.com

大まかな実装方針

Swiftは既にCとObjective-Cとの相互運用をサポートしています。ヘッダをClang Modulesとして読み込むことでSwiftから自然な形で外部言語のAPIを呼び出すことができます。

この機能はSwiftコンパイラにリンクされているlibclangToolingを通じて変換されたClangのASTをSwiftのASTに変換することで実現されています。主なコードはClangImporterモジュールに実装されています。

C++との相互運用も同様のアーキテクチャで実装が進んでいます。

API設計の指針

Ideally, users working in Swift should not feel any difference between native Swift APIs and imported C++ APIs.

https://github.com/apple/swift/blob/main/docs/CppInteroperabilityManifesto.md#goals

SwiftとC++からインポートされたAPIに違いを感じさせないような自然なやり取りを目指しているようです。

また、Rustのrust-bindgenのようなユーザーライブラリとして提供するのではなく、コンパイラに結合された形で提供する選択をすることで、エディタ連携やツーリングの面で滑らかな体験を実現したい、という意図が感じられます。

さらに、"Interop must not be a burden for API vendors."と掲げており、JNICLIFのようなグルーレイヤーをできるだけ作らないことを目指しているようです。

関数シグネチャの変換

関数のインポートは比較的直感的な変換になりそうです。

// C++側定義
void increment(int *value);

// Swift側から見たとき
func increment(_ value: inout Int)

// Swiftのユースケース
func useIncrement() {
  var i = 0
  increment(&i)
}

C++側のポインタ型はnullポインタを許容しますが、Swift側のinoutは決してnullにになることはなく、呼び出し側のSwiftの方が制約が強いため、安全に呼び出すことができます。

ただ、Swift から C++の関数を呼び出す場合は問題になりませんが、逆の場合、つまりC++からSwiftの関数を呼び出す場合問題になることがありそうです。例えば、次のようにC++側のvirtualメソッドをSwift側で実装することを考えてみましょう。

// C++ header.

class Incrementer {
public:
  virtual void increment(int *value) = 0;
};

// Swift side (compiler synthesized)
protocol Incrementer {
  func increment(_ value: inout Int)
}

// Swift side
struct MyIncrementer: Incrementer {
  func increment(_ value: inout Int) {
  }
}

この場合、仮想関数テーブルを経由してSwiftのincrementメソッドをC++から呼び出されることがありますが、C++のポインタ型はnullポインタを許容してしまうため、呼び出されたincrementはSwiftとしてありえない状態になります。マニフェストではこの問題に対して明確な対応は書かれていませんが、妥当な線として inout Int または UnsafePointer<Int>として変換できるようにする、という案が考えられます。

他にもいろいろなバリエーションがありますが、基本的にSwift側の制約の方が強いため、Swift → C++の呼び出しであれば安全に呼び出せそうです。

ムーブセマンティクス

提案自体は長らくOwnershipManifestにあるのですが、Swiftにはムーブセマンティクスが未だ実装されていません。そのため、RAIIパターンでよく使われるmove-onlyなC++の型を綺麗にSwiftの世界にマッピングできません。

moveonly型がSwiftでサポートされるまでの対応として、UnsafePointerに特別な制約を課し、コピーできないようにしつつ、 moveInitialize メソッドでムーブする、という方法が挙げられています。

// C++ header.

// `File` is a move-only C++ class.
class File {
private:
  int file_descriptor_;
public:
  File(std::string_view filename);
  File(const File &) = delete;
  File(File &&) = default;
  ~File();

  File& operator=(const File &) = delete;
  File& operator=(File &&) = default;

  std::string ReadAll();
};

// Swift side (compiler synthesized)
struct File {
  public init(_ filename: std.string_view)
  public func ReadAll() -> std.string
}

// Swift side
func useFile(_ f: UnsafeMutablePointer<File>) {
  // var f2 = f.pointee // compile-time error: can only call a method on 'pointee'.
  print(f.pointee.ReadAll()) // OK

  // Move `f` to a different memory location.
  var f2 = UnsafeMutablePointer<File>.allocate(capacity: 1)
  f2.moveInitialize(from: f, count: 1)
  // `f` is left uninitialized now.

  print(f2.pointee.ReadAll()) // OK
  f2.deallocate() // OK
  // The file is closed now.
}

Swiftにムーブセマンティクスが完全に実装されると次のような形になりそうです。

// Swift side (compiler synthesized)
moveonly struct File {
  public init(_ filename: std.string_view)
  public deinit

  public func ReadAll() -> std.string
}

// Swift side
func useOneFile(_ f: File) {
  print(f.ReadAll()) // OK

  // Move `f` to a different memory location.
  var f2 = move(f)

  // print(f.ReadAll()) // compile-time error: can't access `f`, its value was moved

  print(f2.ReadAll()) // OK
  endScope(f2) // OK
  // The file is closed now.
}

StructとClassのインポート

C++のstructとclassは基本的に同じ機能を提供するためclassについてのみ話します。

C++のclassはSwift的に見ると基本的に値型と同じ振る舞いをするのでSwiftのstructとしてインポートするのが妥当だろうとされています。

ただ、このインポートでは表現できないC++の機能がいくつかあります。

例えば、C++のclassは継承をサポートしていますが、Swiftのstructは継承ができず値型同士のサブタイプ関係は存在しません。そのため、C++のclass型を値レベルで自然にキャストすることはできません。

しかし、そもそも値型同士のサブタイプ関係にはObject slicingの問題があり、ミスが起こりやすいとされています。そのため、安全な操作でないことを示すためにUnsafePointer経由で表現することになりそうです。

// C++ header.
class Base {
    int value1;
};

class Derived : public Base {
    int value2;
};

// Swift side (compiler synthesized)
struct Base {
  var value1: Int32
}

struct Derived {
  var value1: Int32
  var value2: Int32
}

// Swift side
var derived: Derived = ...
var base: Base = UnsafeRawPointer(&derived).load(as: Base.self)

値レベルでのキャストはunsafeな操作ですが、ポインタ同士のキャストはどうでしょうか?

例えば UnsafePointer<Derived>UnsafePointer<Base> の変換はSwiftとしてはサブタイプ関係がないので違法ですが、実体であるC++としては合法なキャストです。

これを実装するプランとして

  1. as 演算子を拡張してC++型のUnsafePointerのキャストを特別扱いする
  2. キャスト用のメソッドをC++の型に生成する
  3. C++型のキャストのためのcompiler intrinsicsな関数を提供する

が挙げられています。

1番のプランは、すでにキャストロジックが複雑になりすぎていることから見送られそうです。

2番のプランはC++からインポートした型に以下のよなAPIを自動生成する、というものです。

// Swift side (compiler synthesized)
public struct Derived {
  @returnsInnerPointer
  public var asBase: UnsafePointer<Base> { get }

  public static func downcast(from basePointer: UnsafePointer<Base>) -> UnsafePointer<Derived>?
}

public struct Base {
  public static func upcast(from basePointer: UnsafePointer<Derived>) -> UnsafePointer<Base>
}

// Usage example.

func useBasePtr(_ basePtr: UnsafePointer<Base>) { ... }
func useDerivedPtr(_ derivedPtr: UnsafePointer<Derived>) { ... }

func testDowncast() {
  var d = Derived()
  useBasePtr(d.asBase)

  // or:
  useBasePtr(Base.upcast(from: &d))
}

func testUpcast(_ basePtr: UnsafePointer<Base>) {
  if let derivedPtr: UnsafePointer<Derived> = Derived.downcast(from: basePtr) {
    useDerivedPtr(derivedPtr)
  }
}

3番目のプランは以下のような特別なキャスト用の関数を提供するという方法です。2番目のプランではキャストメソッドの自動生成時にキャスト可能な型のペアを制限する形になっていますが、このプランでは型チェック時にC++側の定義を元にSourceとDestinationに派生関係があるか否かを調べるようです。

public func cxxUpcast<Source, Destination>(
  _ source: UnsafePointer<Source>,
  to type: Destination.Type
) -> UnsafePointer<Destination>

public func cxxDowncast<Source, Destination>(
  _ source: UnsafePointer<Source>,
  to type: Destination.Type
) -> UnsafePointer<Destination>

public func cxxDynamicDowncast<Source, Destination>(
  _ source: UnsafePointer<Source>,
  to type: Destination.Type
) -> UnsafePointer<Destination>?

テンプレート関数のインポート

C++のtypenameテンプレートをSwiftのジェネリック関数としてインポートする機能が提案されており、実際に部分的に実装されています。

// C++ header.
template<typename T>
void functionTemplate(T t) { ... }

// Swift side (compiler synthesized)
func functionTemplate<T>(_ t: T)

単純な変換に見えるかもしれませんが、C++のテンプレートとSwiftのジェネリック関数には実行モデルに大きな違いがあります。

C++のテンプレートはコンパイル時に静的に型パラメータが決定され展開されますが、対してSwiftのジェネリック関数は型パラメータは実行時に差し替えることが可能です。スペシャライズはあくまで最適化の1つであり、C++のように常に展開されるわけではありません。

つまり、Swiftネイティブなジェネリック関数はpublicな関数としてモジュール外部から型パラメータを注入して使うことができますが、C++由来のジェネリック関数は必ずモジュール内でスペシャライズされる必要があります。

これらを区別するために @_must_specialize アトリビュートを導入し、C++由来のジェネリック関数に付ける提案がされています。

// C++ header.
template<typename T>
void functionTemplate(T t) { ... }

// Swift side (compiler synthesized)
@_must_specialize
func functionTemplate<T>(_ t: T)

テンプレートの展開はClangImporterが型パラメータをSwift型からC++型に変換した上で、Clangの意味解析モジュールでテンプレートをインスタンス化することで実装されています。

例外

-fno-exceptions オプションが付いていない場合、全てのC++関数が例外を投げる可能性があるため、素直にSwiftとして表現するとthrowsだらけになってしまいます。C++にはnoexcept というキーワードもありますが、マニフェストによると慣用的なC++ではないそうです。nothrowが付いてないとしても実態は例外を投げない、といったことが多いので、デフォルトをthrowsにするのは使い勝手が悪そうです。

そこで提案されているのが throws! というキーワードをSwift側に追加するプランです。 throws! はdo-catchで例外をキャッチすることも可能だが、キャッチしないことも可能、を示します。例外が投げられたがキャッチされなかった場合、プログラムは終了するようです。

これについては現在Pitchが出ています。

標準ライブラリ型のブリッジ

Objective-Cとの相互運用ではNSArrayやNSDictionaryなどのNSプレフィックスの付いたObjective-Cの型とSwiftの型の変換がサポートされています。このブリッジングはSwift側の型がObjective-Cの型の所有権を奪うことでほぼノーコストで行われています。

しかし、同じような仕組みを提供するためにはABIの変更が必要となってしまうようです。

まとめ

Interoperabilityの仕様には細かい決めの問題が大量にあって言語デザインのセンスが問われそうだなと思いました。

今回紹介した機能は実装が予定されている機能の一部だけなので、興味がある方は実装やManifestを見ると面白いと思います。

iOSDC 2020に参加/登壇 しました

kateinoigakukunです。4回目のiOSDCに参加しました。3年連続スピーカーとして参加です。

発表内容

プロポーザル

fortee.jp

スライド

デモ

ということで、去年iOSDCでも発表したWebAssemblyネタの最新版をお届けしました。いかがでしたでしょうか。ここ1年間の成果発表のような形になったと思います。40分枠にすればよかったと少々後悔してます 😅

登壇中にDiscordのパブリックビューイングチャンネルでオーディエンスとわいわいできたのは事前収録ならではの新鮮な体験でした。Ask the Speakerでは沢山の方に興味を持ってもらえたようでとても嬉しかったです。早速SwiftWasmを試してくださった方も 👀

github.com

他のスピーカーの発表

最近はコンパイラ周りしか触っていなかったので色々な分野の話を聞けて刺激になりました。まだまだ知らないことや理解が浅いものがあるので勉強していきたいです。

今年もこうして参加できて本当に良かったです。