Xamarin Advent Calendar 2019 24日目のエントリーです。えっオマエ生きてたの?って感じですが、軽めのネタ・調べ物で参戦です。
目次
- what is it?
- 今使えるの?
- どんなバインディングが出来るの?
- swiftライブラリのメタデータ構築
- 独自swiftc拡張
- ラッパーswiftライブラリの生成
- C#バインディング生成
- BTFSの制限
- まとめ
what is it?
2019年はXamarin方面では落ち着いた1年になっていたかと思います。ネイティブ・プラットフォーム側はJetpack ComposeやSwiftUIが発表され、これから新しい時代が始まろうとしていますが、Xamarinは独自のクラシックなエコシステムに立脚するだけで続いていくのか、新しい波にきちんと乗っていけるのか、来年は動きを見せていくべき1年なのだろうなと思います。
Xamarin.Android方面ではbinding generatorからKotlin artifactを取り除いたり、AndroidSupportComponentsからAndroidXサポートを切り出したリポジトリが出現するなどの進展は見られましたが、それ以上に大きな動きは無いようです。まあ退屈と言えば退屈ですね。
一方で、今年公開された中でわたしがダントツに面白そうだと思ったのは、xamarin-macios方面で今年の秋頃に公開されたらしいbinding-tools-for-swiftです。
これまでもtom-swiftyやらSwiftNetifierやら、いろんな名前で開発されてきたSwiftとC#の相互運用のためのツールが、ついに公開されたということですね。tom-swiftyの名前はツール名としても残っているみたいですね(Tom Swiftyという単語はWikipediaにも項目があるくらい一般的であるようです)。
binding-tools-for-swiftに関する公式リポジトリ以外の情報は現状ほぼ皆無ですが、Xamarinで開発を担当してきたメンバー(らしい。わたしがいなくなってから入ってきた人みたい)が導入的なブログポストをまとめています。
現在製品化されているXamarin.iOSとXamarin.MacはObjective-CのAPIに対応するバインディングとバインディング生成機構を、ObjCRuntimeを経由して実現するものですが、Swiftの場合はランタイムも含めさまざまな独自要素が含まれてくるので、新しいバインディング生成ツールが必要とされてきました。
Swift自体はもう何年も前から存在する言語なので、今さらと思われるかもしれませんが、一般的な評価として、Swiftの言語仕様が安定してきたのはSwift3〜4あたりの比較的最近のことであり、言語機能はObjective-Cよりも複雑で、さらにABI安定性もSwift4でようやく目標となったような段階なので*1、バインディングツールに関しては、ようやく本腰を入れて取りかかれる状態になったとも言えます。(ABI不安定なときにがっつり実装しても新バージョンのSwiftで台無しになったら残念なことになるので…)
わたしはAppleユーザーとは言いがたいですしこれまで試したことも無かったのですが、今回はこのbinding-tools-for-swiftをどうやって使うのか、何が面白いのかといった話を書きます。
binding-tools-for-swiftという名前は長いので、以降公式ドキュメントの慣行に準じてBTFSと表記します。
今使えるの?
BTFSはまだ著しく開発中で、おそらく大概のライブラリはバインドに失敗するのではないかと思います。仕様としてバインディング作成が難しい(Xamarin.Androidのように、言語間の違いを吸収して完全自動でバインドするのが、チャック・ノリスにしか出来ない)とかいう以前に、そもそもツールチェインが整っていないとか、エラーログがまともに出力されない、といったレベルの状態なので、われわれ外部の開発者はもう少し整備されるのを待ったほうがよさそうです。
BTFSのビルド手順はトップディレクトリでmake
を実行するだけなのですが、現状では、xamarin-maciosをビルドするのと同様に、特定のバージョンのXcode、monoやXamarin.iOS、Xamarin.Macのダウンロードが必要になります(Xamarinプラットフォームはmasterでも可能そうですが、おそらくBTFS開発者向けです)。Xcodeだけでも15GBくらいは必要になるのでストレージに余裕の無い人は試せないと思います。不足コンポーネントをダウンロードするためのjenkinsスクリプトのコマンドなどを示してくれるので、それに従っていればいずれビルドできるでしょう(適当)。
いったんビルドできたら、mono tom-swifty/bin/Debug/tom-swifty.exe --help
を実行して確認してみるとよいでしょう。
実際にライブラリをバインドするときは、quickstart guideにもある以下のコマンドを実行することになります。
mono /path/to/tom-swifty.exe --swift-bin-path SWIFT_BIN_PATH --swift-lib-path SWIFT_LIB_PATH -o /path/to/output_directory -C /path/to/YOURLIBRARY.framework -C /path/to/binding-tools-for-swift/swiftglue/bin/Debug/PLATFORM/XamGlue.framework -module-name YOURLIBRARY
SWIFT_BIN_PATH
とSWIFT_LIB_PATH
には以下のようなパスを指定することになります。BTFS用に手を加えられた独自のswiftコンパイラが必要になります。
/path/to/binding-tools-for-swift/SwiftToolchain-v3/GITHASH/build/Ninja-ReleaseAssert/swift-macosx-x86_64/bin /path/to/binding-tools-for-swift/SwiftToolchain-v3/GITHASH/build/Ninja-ReleaseAssert/swift-macosx-x86_64/lib
わたしはBTFSをビルドしてコマンドラインで実行できるところまでは進めましたが、バインド出来るライブラリは特に見つけられませんでした。AudioKitあたりはバインディングがあってもいいんじゃないかなーと思ったのですが、framework以下の実行ファイルへのシンボリックリンクを読めないレベルの完成度でした。
一般ユーザーのところに降ってくるまでには、MSBuildタスクなどが完成してMSBuildやVS{Mac, Win}から呼び出せたりNuGetで取ってこられるようになったりしていると思います。
とりあえず、今まだ使えないとしても、BTFSがどんな工程を経てユーザーのライブラリに対するバインディングを生成するのか、という部分は実装のステータスとは別に面白い話になると思うので、このエントリの以降の内容はFunctional Outlineのドキュメントで説明されているビルドプロセスについて、他のドキュメントにも言及しつつ説明していきます。
どんなバインディングが出来るの?
BTFSでは、以下の言語要素がサポートされています。
- クラス
- 構造体
- 列挙型
- 関連型(associatedtype)の無いプロトコル
- トップレベルの関数と変数
- ジェネリックなクラス・構造体・列挙型
- escapingなクロージャー
- @ObjCな型
- 非virtualメソッドにおけるプロトコル合成型(composition types)
- 例外
- 拡張(extensions)
サポートされていないのは↓のような要素です。
- 関連型のあるプロトコル
- noescapeなクロージャー
- クロージャーによるジェネリックインスタンス(bound generic types with closures) (たぶん意味としてはこの訳で合っていると思うけど勘違いがあるかも)
ObjectModelingのドキュメントにおおよその構造が書かれていますが(ちょっと長いのでここでは引用しません)、ISwiftObject
というインターフェースを実装するオブジェクトとなるようです。Xamarin.AndroidにおけるIJavaObject
みたいなものでしょう。(XAの場合は、IJavaObject
自体は単なるJavaオブジェクトのマーカーでしかなく、必ずJava.Lang.Objectクラスから派生することになりますが。)
Objective-CのときもプロトコルはストレートにC#にマッピング出来ない存在でしたが、Swiftのプロトコルへのバインディングもやや複雑な説明を要する存在になっています。Protocol Handlingのドキュメントで詳しくまとめられていますが、EveryProtocol
という何やら神っぽいクラス(!?)を定義して、それを渡して廻すようです。
swiftライブラリのメタデータ構築
Swiftのライブラリをソースからビルドすると、swiftmoduleというライブラリのメタデータを格納したファイルや、SILと呼ばれる中間表現のバイナリファイルになります。BTFSではこれらを対象にバインディングの元になるSwiftType
という型情報を構築していきます。最終的にSwiftType
を集めたものはInventory
と呼ばれるメタデータの集合体になり、次のビルドプロセスであるところの「ラッピング」の入力になります。
もっとも、このアプローチでは十分な型情報を得ることができないので、次にXML relectionという解析プロセスを経て型情報を完成させます。
ちなみに、これらの過程ではswiftのライブラリのバイナリから識別子を取得する必要があるのですが、swiftコンパイラはswiftの識別子をバイナリコードとしてプラットフォーム準拠の形式にするためにmanglingという加工処理を行います(C++コンパイラなどでも行われています)。BTFSにはこれを解析するDemanglerというコンポーネントが含まれています。
独自swiftc拡張
BTFSのドキュメントによると、swiftcが生成するswiftmoduleやライブラリファイルには、バインディングの生成に必要となる型情報が十分には含まれていないため、Xamarinで独自にswiftcに手を加えた(?)コンパイラをビルドするようです。XML reflectionの過程で使用されるのはこのスクリプトでビルドされる…かもしれない…コンパイラです。(通常はビルド済バイナリのダウンロードが走るだけになりそう)
https://github.com/xamarin/binding-tools-for-swift/blob/master/jenkins/build-swift.sh
XmlReflectionのドキュメントを眺めてみると、既存のswiftcに手を加えるというよりはそもそも自前でswiftコンパイラを作るみたいな壮大な(?)話が書かれています。まあパーサを作ってXMLでメタデータを生成するだけなのでそれほど非現実的でもないかも…? わたしもXamarin.Androidのビルド用でJavaスタブコードのパーサー書いたりしましたし(たぶん今でも使われているはず…?)
ともあれ、このXml reflectionのアウトプットはModule Declarationと呼ばれ、これも次のビルドプロセスであるラッパー生成処理への入力となります。
ラッパーswiftライブラリの生成
InventoryとModule Definitionという2つの類似するメタデータ集合が出来上がったら、次はこれらをもとにいよいよバインディングを生成…というわけにはいきません。 ユーザーのswift APIを、いったんC#のP/Invokeで呼び出せるスタイルのAPIでラップしたswiftコードを生成します。この過程はWrappingBuilderという部品が実現しています。
BTFSの中で、メタデータ情報を解析して型ツリー情報を構築するのはDynamoと呼ばれています。このBTFSから、swiftコードを生成したり、後でC#コードを生成したりすることになります。
ラッパー生成処理は、実際には2つのユーティリティで行われています。WrappingBuilderが基本的なクラス構造を定義して、もうひとつOverrideBuilderという部品が、C#でopenクラスを派生できるように必要になる派生クラスを生成します。Xamarin.Androidにもgen-java-stubs.exeとかGenerateJavaStubsというMSBuildタスクがあるのですが、それに相当するものと考えてよさそうです。
C#バインディング生成
ラッパーを準備してAPIメタデータを整地したところで、いよいよC#バインディングの生成に入ることが出来ます。まず、ラッパーのライブラリから、再びSwiftType
のInventoryとreflectorによるModule Declarationの2つのメタデータ集合を生成します(処理内容は最初と同じです。対象がラッパーになっただけです)。ここでまたDemanglerが必要になります。
C#のコードを生成するのは、ラッパーを生成する時に登場したDynamoの役割です。これでようやく完成です!
BTFSの制限
BTFSがサポートしているのはSwift 5までの文法で、Swift 5.1はサポートしていません。SwiftUIが多大に依存しているOpaque Result Typesなどはまだ使えないということですね。
まとめ
今回はXamarin/binding-tools-for-swiftで行われているバインディングのビルド過程を追ってみました。途中で独自のswiftコンパイラハックを加えていたりと、非常にいかがわしい萌え技術が使われているのではないでしょうか。
*1:たとえばこのswiftのname manglingに関するドキュメントを見ると4.0と4.2で違っていることがわかります