RubyKaigiとiOSDCでWasmの話をしてきた
09/08-10に三重で開催されたRubyKaigi、09/10-12に東京で開催されたiOSDCにどちらもスピーカーとして参加してきました。
カンファレンスはしごされた方はお疲れ様でした。
RubyKaigi Keynote
初めてのRubyKaigiでの発表で、さらにキーノートで、さらにトップバッターという大変貴重な体験でした。いやー緊張した。1
当日のスライドはこちら。
Ruby 3.2でサポート予定のRubyのWebAssembly/WASI対応について話してきました。
前半でモチベーションや出来るようになったことをデモを交えつつオーディエンスと共有して、後半は実装について自分の好きなことを話す、という構成でした。
syntax_tree
を使ったデモはちょっと上手くいかなかったんですが 2、
一番見せたかったIRBでSVGを表示するデモがうまくいって良かったです。IRBのデモはこちらで遊べます。
SVG画像綺麗〜 #rubykaigi pic.twitter.com/ODy1I1f7Ar
— 桐生あんず@BOOTH新刊出てます (@anzu_mmm) 2022年9月8日
少しでもKaigiを盛り上げられたなら嬉しいです。発表練習に付き合ってくださった皆さんありがとうございました。
さっそく遊んでくださっている方もちらほら見られたり、「WebAssemblyのことは今まであまり知らなかったけど興味が出てきた」といった声を頂くこともあり、大きな場で話す機会の重要性を実感しました。
最終日途中でiOSDCの方に移動してしまったのでクロージングはきちんと見られていないんですが、TRICKの受賞作品をブラウザで動かすというアツい展開もあったようですね。
気になったトーク
Towards Ruby 4 JIT - RubyKaigi 2022
コンパイラ側の気持ちに興味があるため、JITの話はかなり面白かったです。自分で手軽にJITコンパイラをかける口があるとのことなので試してみたいと思います。特にLLVMよりコンパイル時間が安く済むと噂のCraneliftは一度試してみたいので。
Implementing Object Shapes in CRuby - RubyKaigi 2022
Object Shapeの話も直感的にはグッと速くなるのかなと思っていたんですが、意外と伸び悩んでいたのでどこに原因があるのか、そもそもここにネックはないのか?と色々と考えることがあって面白いなと。これも追っていきたいです。
Stories from developing YJIT - RubyKaigi 2022
例によってこれも実際には聴けていないんですが、反応を見る限りかなり自分の興味に合いそうなので気になっています。動画が出たらすぐに見たい。
おいしいRubyKaigi
ありがたいことに大変おいしい思いをさせていただきました。 うなぎも松坂牛もウマー😋 ご馳走様でした。
iOSDC
RubyKaigi最終日の昼過ぎに会場を出て、iOSDC初日の終わりの方から参加していました。iOSDCでは今年で通算4回目の発表でした。
懲りずに自分がメンテしているSwiftWasmプロジェクトについての発表だったんですが、今年はプロダクションでの採用事例やデモを多めにすることで、「あれ、意外ともう使えるんじゃないか?」と思ってもらうことがメインの狙いでした。どうでしたか?
また、実際に手を動かしてコントリビューションしてくださる例もあり、大変ありがたかったです。
iOSDCで @kateinoigakukun が、swiftwasmは多様な難易度/レイヤで取り組めるからやってみてねって言っていたので、チャレンジしてみたら僕でも出来る事がありました!
— noppe (@noppefoxwolf) 2022年9月14日
今日からswiftwasmコントリビューターやな!!https://t.co/k3R92PUppf
気になったトーク
Xcode が遅い! とにかく遅い!! 遅い Xcode をなんとかする方法 by Yoshimasa Niwa | トーク | iOSDC Japan 2022 - fortee.jp
ある程度大きなプロジェクトを経験したことがあったので、共感できることも多かったです。正しく根本の原因を特定し、現実的なワークアラウンドに落とし込んでいく様子はソフトウェアエンジニアリング感がありこうありたいなと思いました。
まとめ
RubyKaigiではほぼ皆さんはじめましての状態だったんですが、おかげさまで楽しいコミュニケーションができました。これからもよろしくお願いします。 iOSDCでは久しぶり皆さんにもご挨拶できたのでオフライン開催ありがたかった…
来年もどちらも参加したいです。(幸い来年は日程が離れてそう)
-
緊張の結果がこれです https://twitter.com/kateinoigakukun/status/1567708097900331008↩
-
これはいまだに原因が分からない… ライブコーディングはむずかしい。↩
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++
-
これは結局muslのスレッドスタックサイズのデフォルト値がglibcよりかなり小さくてスタックオーバーフローしてただけだった https://github.com/WebAssembly/binaryen/issues/4401↩
-
これはOfficialではないがOpenJDKとかでも使われてる↩
Rubyのコミッタになりました
実は先日 ko1さんとmameさんから推薦をいただき、Rubyのコミッタになりました。
ここ数ヶ月間、Rubyアソシエーションの開発助成プロジェクトとしてCRubyのWASIサポートを進めており、WASIのプラットフォームメンテナが必要ということで。
WASI対応の話はPublickeyさんに良い感じにまとめていただきました。詳しい実装の話は後日書こうと思います。1
正直Rubyのことを聞かれても答えられる自信はありませんが、CRubyの実装はほんのり分かるようになりました。あとビルドスクリプトはどこもつらい。2
という訳で、晴れてSwiftとRubyのコミッタという謎の人材になりました。 引き続きどちらもやっていくので、よろしくおねがいします。
-
書きました An Update on WebAssembly/WASI Support in Ruby | by kateinoigakukun | ITNEXT↩
-
SwiftはカスタムターゲットまみれのCMake、CRubyは4000行以上のconfigure.acが…↩
TypeProf for IDEの開発をお手伝いしました at クックパッド
TL;DR
インターンの内容
TypeProfはクックパッドでフルタイムRubyコミッタをされている@mametterさんが開発しているRubyの型プロファイラです。 Rubyのプログラムにできるだけ型注釈を入れずに抽象解釈によって型を推論する、という面白い特徴があります。
Ruby 3.0ではRBSのプロトタイプを生成するためのツールとしてRubyにバンドルされています。
今回のインターンでは、TypeProfの解析結果を利用したRubyのLanguage Serverの実装をお手伝いしました。
TypeProf for IDEについては今年のRubyKaigi Takeout 2021のKeynoteで発表があったので、雰囲気を知りたい方はこちらを見てください。
www.slideshare.net
実装
そもそもRubyを触るのが久々かつ、TypeProfがそこそこ大きいプログラムであったため、binding.irb
でデバッグ可能な環境を作るところからはじめました。
これを初手で準備したことでその後のコードリーディング効率が格段に上がりました。やはりデバッガは偉大。
その後mameさんに助けてもらいながら数種類のコードジャンプを実装しました。 ジャンプ先候補はTypeProfの解析結果を使っているので、解析器自体の実装も勉強できて良かったです。
- LSP: Support local variable definition jump
- LSP: Support instance variable definition jump
- LSP: Support Find References for methods
- LSP: Support class/const definition jump
もうひとつ面白い改善として、コード補完の高速化を行いました。
TypeProf for IDEではユーザが1タイプするごと (textDocument/didChange
) にプログラムを解析し直すのですが、解析の仕組み的に一回の解析にかなり時間がかかります。
オリジナルの実装では解析中サーバスレッドをブロックしていたため、タイプするごとに解析待ちのリクエストが溜まっていました。
さらに、コード補完 (textDocument/completion
)では変更後のプログラムを検証する解析とは別の解析がサーバスレッドで行われているため、1タイプごとに貯まる重いタスクが更に増え、若干もっさりしていました。
LSPではdidChange
は通知であり、すぐに結果を返却しなくても良く、更にコード補完リクエストもリクエストとレスポンスの順序を気にしないメソッドなので、高速化のために解析スレッドを分けることにしました。
また、新しい didChange
通知が来た時点で前の状態での解析結果は無効になるため、解析を途中で打ち切る機構を追加しました。
before | after |
---|---|
その他にも色々と実装できて楽しかったです。
Ruby本体への貢献
TypeProf for IDEは最新の開発版MRI (Matz's Ruby Interpreter) のAPIを使っており、CIではデイリービルドされた最新のRubyバイナリを使っています。
具体的には、ruby/setup-ruby のGitHub Actionsのアクション経由で ruby/ruby-dev-builder でビルドされた成果物を使っていました。
しかし、ある日直近で入ったAPI変更に追従する変更をTypeProfに入れたところ、CI上のテストが失敗し始めました。
調査したところ、ruby/ruby-dev-builderのデイリービルドがmacOS上で数十日間失敗しており、最新のバイナリが全くアップロードされていないことがわかりました。具体的には、macOSにデフォルトで同梱されているGNU Makeのバージョンが古いことが原因で、最新のruby/rubyのMakefileを正しく解釈できていませんでした。
そして、特定のビルドオプションを付けたときのみ再現する問題であったため、ruby本体のCIを奇跡的に通り抜けていました。
(macOSはGPLv3を避けるためにGNU Makeのバージョンを3.81で止めている。ちなみに3.81は2006年リリース。最新は4.2)
とりあえずの対処としてGNU Make 3.81で最新のバージョンと同様の動作をさせるためのワークアラウンドを追加しました。
ということで晴れて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
文を書くだけで、インポートしたモジュールに紐づくライブラリがリンクされます。
XcodeでiOSアプリを書く際、明示的に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を壊さずに依存ライブラリを変更出来るようになります。
詳しい議論については以下のフォーラム投稿を見てください。
ここで、次のケースを考えます。モジュール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の静的リンク時にシンボル不足エラーが出ています。
修正編
この問題を解消するためには、Foundationにicui18nへのライブラリ依存(=LINK_LIBRARYエントリ)を持たせれば良いわけです。
しかしSwift 5.4当時のコンパイラに任意のライブラリの LINK_LIBRARY
エントリをモジュールに差し込む機能はありませんでした。
そこで、Swift Forumで解決策について議論した上で、コンパイラに新しいオプションを追加実装しました。
このパッチでは、-public-autolink-library
というオプションを追加して、指定したライブラリを LINK_LIBRARY
エントリ に出力するようにしています。
コンパイラ側のパッチがマージされると、ようやくFoundation側の修正に取り組めます。
このパッチでは、追加したオプションを使って icui18n
へのライブラリ依存をFoundationに足しています。
その後リバートやリバート返しを経て、最近ようやく直りました。
まとめ
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の実装状況と、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では.drectve
、Mach-OではLC_LINKER_OPTION
に降下します。
一応ELFでも SHT_LLVM_LINKER_OPTIONS
という特別なセクションにリンカオプションが埋め込まれるようにコード生成されますが、なんと肝心のlld側が対応していません。
他のオブジェクトファイルの形式と違い、任意のリンカオプションを受け付けるのではなく、オプションのセマンティクスを明示的にしたKey-Valueペアで表現するため、
コンパイラフロントエンドがELF向けに特別な処理を入れる必要があります。
SwiftのWindowsポートで有名なcompnerdさんが2018年に提案して、ClangフロントエンドとLLVMバックエンドの実装をしましたが、その後アップデートが無いようです。
- LLVM Language Reference Manual: Automatic Linker Flags Named Metadata
- [llvm-dev] Linker Option support for ELF
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
を使う実装になっています。
まとめ
大変