kateinoigakukunのブログ

思考垂れ流しObserver

RubyKaigiとiOSDCでWasmの話をしてきた

09/08-10に三重で開催されたRubyKaigi、09/10-12に東京で開催されたiOSDCにどちらもスピーカーとして参加してきました。

カンファレンスはしごされた方はお疲れ様でした。

RubyKaigi Keynote

初めてのRubyKaigiでの発表で、さらにキーノートで、さらにトップバッターという大変貴重な体験でした。いやー緊張した。1

当日のスライドはこちら。

Ruby 3.2でサポート予定のRubyのWebAssembly/WASI対応について話してきました。

前半でモチベーションや出来るようになったことをデモを交えつつオーディエンスと共有して、後半は実装について自分の好きなことを話す、という構成でした。

syntax_treeを使ったデモはちょっと上手くいかなかったんですが 2、 一番見せたかったIRBSVGを表示するデモがうまくいって良かったです。IRBのデモはこちらで遊べます。

irb-wasm.vercel.app

少しでも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プロジェクトについての発表だったんですが、今年はプロダクションでの採用事例やデモを多めにすることで、「あれ、意外ともう使えるんじゃないか?」と思ってもらうことがメインの狙いでした。どうでしたか?

また、実際に手を動かしてコントリビューションしてくださる例もあり、大変ありがたかったです。

気になったトーク

Xcode が遅い! とにかく遅い!! 遅い Xcode をなんとかする方法 by Yoshimasa Niwa | トーク | iOSDC Japan 2022 - fortee.jp

ある程度大きなプロジェクトを経験したことがあったので、共感できることも多かったです。正しく根本の原因を特定し、現実的なワークアラウンドに落とし込んでいく様子はソフトウェアエンジニアリング感がありこうありたいなと思いました。

まとめ

RubyKaigiではほぼ皆さんはじめましての状態だったんですが、おかげさまで楽しいコミュニケーションができました。これからもよろしくお願いします。 iOSDCでは久しぶり皆さんにもご挨拶できたのでオフライン開催ありがたかった…

来年もどちらも参加したいです。(幸い来年は日程が離れてそう)


  1. 緊張の結果がこれです https://twitter.com/kateinoigakukun/status/1567708097900331008

  2. これはいまだに原因が分からない… ライブコーディングはむずかしい。

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を使う実装になっています。

まとめ

大変

参考図書