こんにちは、メルペイエキスパートチームインターンの@kateinoigakukunです。
本記事では私がインターンの中で開発しているSwift言語のWebAssembly対応について紹介します。
背景
メルペイでは「技術をアウトプットするところに技術は集まる」という思いから、 稼働の50%以上を技術コミュニティへの貢献や技術の普及に取り組むエキスパートチームが存在します。
私は主にSwiftコミュニティへの技術的な貢献に注力しています。特に去年の10月頃からSwiftのWebAssemblyサポートを進めています。
WebAssemblyはブラウザで実行可能なバイナリ形式として開発されている低級言語です。またブラウザ以外の環境でも実行できるポータブルな形式としても設計されており、活用できる用途は多岐にわたります。
Swiftは教育の現場でも取り扱われるケースが増えてきており、注目されている言語の一つです。
これは個人的な見解ですが、既にiOSやmacOSプラットフォームでフロントエンドのアプリケーション層の実績があるSwiftは、Webアプリケーション開発にもフィットするのではないかと考えています。そのため、WebAssemblyに対応し、SwiftによるWebアプリケーション開発が実現できれば、エコシステム次第で新たなWeb開発の選択肢になりうると考えています。
また、他のモダンな言語に比べて扱えるプラットフォームが少ない、という課題があるSwiftですが、ポータブルな形式であるWebAssemblyに対応することで、OSやCPUアーキテクチャを問わず様々な環境で実行できるようになります。
Swiftが活躍できる環境を増やしていくことで、初学者がSwift PlaygroundでSwiftを学んだ後の次のステップの選択肢が増えると良いな、という思いでSwiftのWebAssembly対応を進めています。
SwiftWasmプロジェクトについて
SwiftWasmプロジェクトは昨年@zhuoweiさんが立ち上げたプロジェクトです。SwiftコンパイラやLLVMをフォークしてWebAssembly対応を進めつつ、アップストリームにパッチを送っています。
ビルド済みのツールチェーンを毎日配布しており、手軽に試すことができます。
進捗報告
標準ライブラリのテストケースがすべて成功するようになりました
取り組み始めた当初は250ケース中200ケースほど失敗していたテストケースが、すべて成功するようになりました。
Swiftの標準ライブラリはSwiftの言語機能やテクニックをフルに使っており、テストケースも充実しているため、これらのテストケースが通れば大抵のコードが動くだろう、といった温度感です。
WebAssemblyは他の命令セットと比べ高級な命令が多く、また制約も多いため、LLVMの出力ターゲットを切り替えるだけでは吸収しきれず、コンパイラ側で対処する必要がありました。
標準ライブラリをWebAssembly向け対応でネックになった部分をいくつか紹介します。
Swiftの呼び出し規約
Swiftの呼び出し規約はCの呼び出し規約をベースに制定されています。しかしSwift固有のルールとして、末尾に特別なLLVM属性が付いたオプショナルな引数の追加を許すようになっています。
特別な属性について紹介する前に、前提としてSwiftの関数間の型強制について紹介します。
例えば以下のコードでは (Int?) -> Void
型が (Int) -> Void
型のサブタイプであるという規則を使って、 関数 f
にb
を引数として渡しています。
func f(_ a: (Int) -> Void) -> Void { a(1) } func g(_ b: (Int?) -> Void) -> Void { f(b) }
サブタイプ関係にあるクロージャ型の変換時には、内部的に変換用の関数を間に挟むことで実行時に、引呼び出し側と関数定義側の引数の型が一致するようになっています。Swift風に記述すると以下のような処理が実行時に挿入されます。
func f(_ a: (Int) -> Void) -> Void { a(1) } func g(_ b: (Int?) -> Void) -> Void { func thunk(_ x: Int) -> Void { // 0. Capture 'b' // 1. Transform arguments let y = Optional<Int>.some(x) // 2. Apply return b(y) } f(thunk) }
さて、以下のコードではオプショナルの例と同様に (Int) -> Void
型が (Int) throws -> Void
型のサブタイプであるという規則を使って、関数 f
に b
を引数として渡しています。
func f(_ a: (Int) throws -> Void) -> Void { try! a(1) } func g(_ b: (Int) -> Void) -> Void { f(b) }
オプショナルの例では変換用の関数が間に注入されましたが、実は throws
の型強制では変換用の関数は生成されず、throws
の付いていない関数がthrows
付き関数として直接呼び出されます。比較的頻繁に使われるthrows
の型強制を、余分な関数を挟まずに実行できるため、実行時パフォーマンスが改善されます。
throws
が付いた関数はエラーオブジェクトを返却するためのポインタを引数に取り、例外発生時にエラーオブジェクトを書き込みます。しかし、通常の引数と同様にスタックに載せる場合、 throws
なしの関数との互換性がなくなり、変換を挟まなければいけなくなります。
冒頭に述べた特別なLLVM属性は、この仕組を実現するために使われています。
特別な属性 swifterror
で修飾されたエラーオブジェクトのポインタ引数は、特定のレジスタに載せられるようになります。そのため、 throws
なしの関数はレジスタを無視するだけで throws
付きの関数との互換性が実現されます。
Swift風の表現をすると以下のコードになります。
func f(_ a: (Int, swifterror inout Error?) throws -> Void) -> Void { var error: Error? = nil a(1, &error) if error != nil { fatalError() } } func g(_ b: (Int) -> Void) -> Void { f(b) }
しかし、WebAssemblyはレジスタを持たないため、エラーポインタ専用のレジスタを割り当てることができません。
さらに、WebAssemblyランタイムは関数ポインタを介した間接的な関数呼び出し時に、関数のシグネチャと実際に呼び出しに使う引数の数が一致しない場合、セキュリティ面を考慮してクラッシュしてしまいます。
この問題を解決するために、
- Swift内部の中間言語(SIL)レベルで変換用の関数を挟む
- 中間言語(SIL)からLLVM IRに降下するタイミングで変換用の関数を挟む
といったアプローチを試しましたが、最終的にLLVMからWebAssemblyに降下するタイミングで、すべてのSwift呼び出し規約を使う関数と関数呼び出しにダミーの引数とパラメータを追加する、というアプローチを採用しました。
SILレベルで変換用の関数を挟むアプローチは最適化オプションを付けない場合うまくクラッシュを回避できていました。しかし、最適化パスの中に throws
関数への型強制命令を含むコードへ最適化するものがあり、最適化オプションを付けた途端に破綻しました。
すべての最適化パスに手を加えるのはメンテナンス性を損なうため、次にSILからLLVM IRへ降下するタイミングで変換用の関数を注入しました。抽象レベルが低くなるほど考慮すべきポイントが増えてくるため、前のアプローチよりも難易度は上がりましたが、無事に最適化オプションありでも合法なコードが出力されるようになりました。
しかし、アップストリームにパッチを送る前にフォーラムで実装方針を議論した結果、SwiftコアチームのJoe Groffさんから、コンパイラ内部でthrows
関数のABI互換性を利用している箇所がいくつかあるので、変換用関数を挟むのではなく、WebAssemblyレベルでシグネチャを一致させる方が良いのでは?、というアドバイスを頂きました。このような経緯で、LLVMからWebAssemblyに降下するタイミングでダミーの引数とパラメータを追加してABI互換性を取る、というアプローチの採用に至りました。
非常に長い道のりでしたが、綺麗な対応になったと思います。また、LLVMにもコントリビュートできたのも個人的に大きな成果でした。
相対ポインタ
相対ポインタはポインタ自身のアドレスから対象のアドレスまでのオフセットを保持するポインタ形式です。Swiftのメタデータに含まれるポインタはほぼ全てこの相対ポインタ形式になっています。
通常のポインタの代わりに相対ポインタを使うメリットはいくつかあります。
- バイナリサイズの節約 (64bitアーキテクチャ上でのみ)
- ロード時再配置を削減
- メタデータを読み取り専用のセグメントに配置できる
まず、相対ポインタは二点間のアドレスの差を計算するリンカの再配置の仕組みを使っています。この再配置の結果は符号付き32bit整数におさまる、という前提で作られているため、通常のポインタが64bit消費するのに対し、相対ポインタはその半分の32bitで表現できます。
また、通常のポインタはロード時に再配置が発生するため、プログラムの起動までにオーバーヘッドが発生します。一方、二点間のアドレスの差は位置独立なデータなので、ロード時の再配置が発生しません。
さらに、ロード時の再配置が無くなることでメタデータをバイナリ上の読み取り専用のセグメントに配置できるようになります。
しかしWebAssembly用のリンカである wasm-ld
は二点間のアドレスの差を計算する再配置レコードをサポートしていません。また、現状WebAssemblyは32bitアーキテクチャのみなのでバイナリサイズの節約はできず、動的リンクもサポートしてないためロード時の再配置が元々ありません。
WebAssembly上で相対ポインタを使うメリットは現状無いため、相対ポインタを使っているコードをすべて通常のポインタを使うように修正しました。
この書き換えを漏れなく完了させることが最も大変でした。書き換えを始めた当初はprint
関数すらまともに動かなかったため、デバッグは困難を極めました。
最終的にデバッガを自作することになりました。
Swift Package ManagerがWebAssembly向けに動くようになりました
Swiftで開発を行う際に必要不可欠なSwiftPMがWebAssemblyに対応しました。
SwiftPMはパッケージマネージャーだけでなくビルドシステムとしての役割も果たすため、コンパイラフラグの受け渡し調整などの作業が必要でした。
これによりWebAssembly向けのライブラリ開発や既存ライブラリ資産の活用がスムーズに出来るようになりました。
JavaScriptとのバインディングライブラリを実装しました
WebAssembly向けに動くようになったSwiftPMを使って、JavaScriptと通信するためのバインディングライブラリを実装しました。
これによりSwiftからDOM操作などのJavaScriptの世界のAPIを使えるようになりました。 冒頭で書いたSwiftでWebアプリケーションを開発するため下地です。
試しに簡単なLife GameをSwiftで書いてみました。(バイナリサイズが大きいため、ロードに時間がかかります。)
life-game-with-swiftwasm.netlify.com
github.com
このライブラリではJavaScriptのAPIをできるだけ自然にSwiftから扱うためにDynamic Member LookupというSwiftの言語機能を使っています。
import JavaScriptKit let alert = JSObjectRef.global.alert.function! let document = JSObjectRef.global.document.object! let divElement = document.createElement!("div").object! divElement.innerText = "Hello, world" let body = document.body.object! _ = body.appendChild!(divElement) alert("Swift is running on browser!")
現状の課題
標準ライブラリも動くようになり、周辺環境も対応が進みましたが、未だ多くの課題を抱えています。
特にバイナリサイズは大きな課題です。現状、Hello, worldのプログラムでさえ10MBを超えています。実際のプログラム自体は2.3KB程度ですが、実行されない標準ライブラリのコードが静的にリンクされてしまっているためにバイナリサイズが膨らんでしまっています。WebAssemblyレベルでのDead Code Eliminationを行うことで4MBまで削減できますが、それでもなお未使用のコードが含まれています。これはSwift固有の実行時メタデータが持つ関数テーブルに実際には実行されないコードが含まれてしまっているためです。
この課題を解決するために、WebAssemblyレベルでの最適化ではなく、言語固有のLink Time Optimizationを構想しています。標準ライブラリとメインのプログラムをリンクする際に、Swiftのメタデータ構造の知識を有した最適化を行うことで、より積極的なDead Code Eliminationが実現できると考えています。
まとめ
SwiftのWebAssembly対応が大きく前進しました。まだまだ最適化の余地があるので引き続き改善していきたいと思っています。
このようなコミュニティに対する貢献がSwiftコミュニティの活性化および技術の発展につながると嬉しいです。
純粋に言語の新しいプラットフォームサポートは非常に楽しいです。このようなコミュニティに対する貢献がSwiftコミュニティの活性化および技術の発展に少しでもつながると嬉しいです。