前回の FlutterからRustを呼んでみた で、C++とRustから同じ形のライブラリを作りました。macOSでは .dylib、Windowsでは .dll が出てきます。

書きながら、ずっと引っかかっていたことがあります。この2つは同じものなのか、違うものなのか。ついでに言えば .a.lib.so もあって、拡張子だけで5種類あります。

調べると「静的ライブラリと動的ライブラリがあります」という説明にはすぐ辿り着きます。でも、それを読んでも何が嬉しくて使い分けるのかが分からないのです。

そこで今回は、小さなライブラリを1つ作って、静的と動的の両方で組み立て、実際に測りました。macOSとWindowsの両方でやっています。

結論から言うと、覚えるべき軸は「いつ名前が解決されるか」の1つだけでした。拡張子の違いは、そこから全部派生しています。

ついでに、予想が3回外れました。どれも記事の見どころになったので、そのまま書きます😅

拡張子を並べても、何も分からない

スポンサーリンク

よくある説明はこうです。

静的動的
macOS.a.dylib
Linux.a.so
Windows.lib.dll

この表は正しいのですが、これを覚えても何も分かりません。「じゃあどっちを使えばいいのか」に答えていないからです。それに「Windowsで .dylib は扱えないのか」という素朴な疑問にも答えていません。これは後半で実際に試します。

eightがこの表で長く困っていたのは、Windowsの .lib だけ2つの意味があることでした。静的ライブラリも .lib、DLLと一緒に出てくるファイルも .lib。これは後半で実測します。

用語集

先に言葉を並べておきます。

用語平たく言うとなぜそれが効くのか
リンク「この名前の関数はここにある」という結びつけ作業これがいつ行われるかが、この記事の全部
シンボル関数や変数につけられた名前の札リンクは札を突き合わせる作業。nm で一覧できる
オブジェクトファイル.o / .objソース1本をコンパイルした中間生成物静的リンクが取り込む単位がこれ(関数ではない)
静的ライブラリ.a / .libオブジェクトファイルを束ねた書庫ビルド時に中身が本体へ写し取られる
動的ライブラリ.so / .dylib / .dllそれ自体では起動できない、部品としてのプログラム起動時か実行中にOSが読み込んで結びつける
ローダプログラムを起動するときにOSで動く仕掛け動的ライブラリを探して結びつけるのはこの人
インポートライブラリ(Windows)DLLの入口の名簿だけが入った .lib★コードは1バイトも入っていない。静的ライブラリと紛らわしい
COMDAT(Windows)「ここは独立して捨ててよい塊です」という印★これが付いていないと、リンカは要らないものを捨てられない

★軸は1つだけ:いつ名前が解決されるか

種類がたくさんあるように見えますが、違いは「ml_version() という名前が、いつ実体と結びつくか」だけです。時点は3つあります。

ライブラリの違いは「いつ名前が解決されるか」 同じ ml_version() を呼ぶのに、結びつく時点が3つある 静的リンク .a / .lib ソース main.c ビルド コンパイル+リンク ★ここで結びつく 起動 OSのローダ 実行中 プログラム自身 動的リンク .so / .dylib / .dll ソース main.c ビルド コンパイル+リンク 起動 OSのローダ ★ここで結びつく 実行中 プログラム自身 実行時リンク dlopen / LoadLibrary ソース main.c ビルド コンパイル+リンク 起動 OSのローダ 実行中 プログラム自身 ★ここで結びつく ★この「時点」の違いが、差し替えの可否・起動時間・配布物の数を全部決めている。
ライブラリの違いは「いつ名前が解決されるか」の1点に集約できる

この3つが、そのまま使い方の3通りになります。

  • ビルド時(静的リンク)… ライブラリの中身が本体へ写し取られる。できあがった実行ファイルは、もうライブラリを必要としない
  • 起動時(動的リンク)… 実行ファイルには「ml_version がどこかに要る」とだけ書いてある。OSのローダが探して結びつける
  • 実行中(実行時リンク)… プログラムが自分で「このファイルを読み込め」「この名前の関数をくれ」と頼む。プラグインの仕組みはこれ

この「時点」の違いが、差し替えができるか・起動が速いか・配るファイルが何個になるかを全部決めています。

実験の材料

小さなライブラリを1つ作りました。関数は4つだけです。

int  ml_add(int a, int b);        // 足す
long ml_fib(int n);               // フィボナッチ
const char *ml_version(void);     // 版を返す("1.0")

// ★アプリからは呼ばない関数。取り込みの単位を調べるために置いてある
int  ml_unused_big(int seed);

仕掛けは最後の ml_unused_big です。中に 16 KiB の表を持たせてあります。

static const int TABLE[4096] = { 1 };   // 4096 × 4バイト = 16,384バイト
int ml_unused_big(int seed) {
    int acc = seed;
    for (int i = 0; i < 4096; i++) acc = (acc * 31 + TABLE[i]) % 1000003;
    return acc;
}

アプリはこの関数を一度も呼びません。呼ばないものが実行ファイルに入るのかどうかを見るための重りです。

これを静的ライブラリと動的ライブラリの両方で作り、使う側を3通り用意しました。

★「いつ結びつくか」を目で見る

言葉で説明するより、これを見るのが早いと思います。

ライブラリだけを 1.0 から 2.0 に作り直して、アプリは再ビルドせずに実行しました。

[作り直す前]
  app_static  : ml_version = 1.0
  app_dynamic : ml_version = 1.0

  → libmath.dylib だけを 2.0 で作り直した

[作り直した後]
  app_static  : ml_version = 1.0     ← 変わらない
  app_dynamic : ml_version = 2.0     ← 追従した
  app_runtime : ml_version = 2.0     ← 追従した

静的リンクだけが 1.0 のままです。ビルドしたときに、ライブラリの中身が実行ファイルへ写し取られてしまったからで、もう外のファイルを見ていません。

Windowsでも同じ結果でした。

ここが「なぜ動的にするのか」の答え
アプリを配ったあとで、ライブラリだけを差し替えて直せます。OSが提供している機能が動的ライブラリなのも同じ理由で、OSの更新でアプリを作り直さずに済みます。
逆に言えば、静的リンクは「配った時点の中身で固まる」。これは弱点にも強みにもなります。

★拡張子は何を決めているのか

ここで、冒頭の表に戻ります。macOSは .dylib、Linuxは .so、Windowsは .dll

そうすると気になりませんか。Windowsで .dylib は扱えないのか。Macで .so は扱えないのか。

やってみるのが早いので、試しました。

拡張子ではなく、中身の形式が決めている ローダが見ているのはファイル名ではなく、先頭に書かれた形式の印 Mach-O macOS が読める形式 .dylib が慣習 ELF Linux が読める形式 .so が慣習 PE-COFF Windows が読める形式 .dll が慣習 macOS で実際にやってみた Macで作った .dylib → .so に改名 読めた Mach-O のまま Macで最初から .so という名前で作る 読めた Mach-O 本物のWindows .dll を Mac で読む 読めない slice is not valid mach-o file その .dll を .dylib に改名して読む 読めない 同じエラー(名前は無関係) ★「Windowsで .dylib は扱えないのか」の答えは、名前としては扱える・形式としては不可能。
拡張子ではなく、中身の形式が「読めるか」を決めている

まず、Macで作った .dylib を、名前だけ .so に変えて読み込ませます。

$ cp libmath.dylib /tmp/libmath_renamed.so
$ ./app_runtime /tmp/libmath_renamed.so
ml_version = 1.0
ml_add(2,3) = 5

普通に読めました。最初から .so という名前で作っても同じです。

$ file /tmp/libmath_renamed.so
/tmp/libmath_renamed.so: Mach-O 64-bit dynamically linked shared library arm64

中身は Mach-O のままです。名前を変えても中身は変わりませんし、OSは中身のほうを見ていました

では逆に、本物のWindowsのDLLを持ってきたらどうなるか。手元のWhiskyのボトルから借りてきました。

$ file real_windows.dll
real_windows.dll: PE32 executable (DLL) (console) Intel 80386, for MS Windows

$ ./app_runtime /tmp/real_windows.dll
dlopen 失敗: dlopen(/tmp/real_windows.dll, 0x0002):
  tried: '/tmp/real_windows.dll' (slice is not valid mach-o file)

読めません。「Mach-Oとして正しくない」と言われています。

ではこれを .dylib に改名したら?

$ cp real_windows.dll real_windows.dylib
$ ./app_runtime /tmp/real_windows.dylib
dlopen 失敗: dlopen(/tmp/real_windows.dylib, 0x0002):
  tried: '/tmp/real_windows.dylib' (slice is not valid mach-o file)

まったく同じエラーでした。名前は1ミリも効いていません😅

★拡張子は、人間向けの目印でしかない
OSのローダが見ているのはファイルの先頭に書かれた形式の印です。世の中には3つあります。
Mach-O(macOS)/ELF(Linux)/PE-COFF(Windows)。
拡張子はその慣習的な名前づけであって、合っていなくても動くし、間違っていても直りません

というわけで、冒頭の疑問への答えはこうなります。

疑問答え
Windowsで .dylib を扱えるか名前としては扱える(中身がPEなら読み込める)。Mac用のdylibは不可能
Macで .so を扱えるかMac用に作った .so なら扱える(上で実証)。Linux用の .so は不可能

静的ライブラリも同じ構図でした。.a の中を開くと、入っているのはそのOS向けの部品です。

$ ar x libmath.a && file mathlib.o
mathlib.o: Mach-O 64-bit object arm64

つまり「静的か動的か」はどのOSでも共通の考え方で、「どのOSで読めるか」は中身の形式が決めている。この2つは別の話でした。表を1枚にまとめると混ざってしまうところです。

ちなみに「読めない」と書きましたが、読めるようにする仕組みは存在しますWhiskyGame Porting Toolkit が、まさにPE形式をmacOS上で読み込んで動かすものでした。あれはこの形式の壁を翻訳して越えている、という位置づけになります。

中身を覗く:.a は書庫、.dylib は実行できないプログラム

それぞれ何者なのかを、コマンドで確かめます。

$ file out/libmath.a out/libmath.dylib
out/libmath.a:     current ar archive random library
out/libmath.dylib: Mach-O 64-bit dynamically linked shared library arm64

静的ライブラリは「書庫」だと言われます。文字どおりで、中を開くとオブジェクトファイルがそのまま入っています。

$ ar t out/libmath.a          $ ar t out/libmath_split.a
__.SYMDEF SORTED              __.SYMDEF SORTED
mathlib.o                     ml_add.o
                              ml_fib.o
                              ml_version.o
                              ml_unused_big.o

左は全部を1つの .c にまとめた版、右は関数ごとにファイルを分けた版です。同じ4関数なのに、書庫の中身の数が違います。これが次章の伏線になります。

誰が何を持っていて、誰が何を欲しがっているかも見えます。

$ nm -gU out/libmath.dylib     # このライブラリが外に出している関数
0000000000000378 T _ml_add
0000000000000380 T _ml_fib
00000000000003c8 T _ml_unused_big
00000000000003bc T _ml_version

$ nm -u out/app_dynamic        # このアプリが外に求めている関数
_ml_add
_ml_fib
_ml_version
_printf

アプリ側に _ml_unused_big が無いのが分かります。呼んでいないので、求めてもいない。当然に見えますよね。ところが——

★★静的リンクは「関数」ではなく「ファイル」単位で取り込む

同じ4関数を、①1つのファイルにまとめた版②関数ごとに分けた版で静的リンクしました。中身は同じなので、同じ大きさになるはずです。

静的リンクは「関数」ではなく「オブジェクトファイル」単位で取り込む 呼んでいない関数(16KiBの表つき)を、同じファイルに置くか分けるかだけの違い macOS(clang) 1つにまとめた .o 50,136 バイト 関数ごとに分けた .o 33,552 バイト 差 16,584 Windows(MSVC) 1つにまとめた .o 157,184 バイト 関数ごとに分けた .o 140,800 バイト 差 16,384 ★Windowsの差はぴったり 16,384 バイト=16 KiB=int×4096 の表そのもの。   呼んでいない関数が、同じファイルに同居しているだけで付いてくる。   clang も MSVC も既定は同じ挙動だった(絶対値の差はCランタイムの下駄)。
同じ4関数なのに、ライブラリの作り方だけで実行ファイルの大きさが変わる

ならないのです😳

macOSWindows
1つにまとめた .o / .obj50,136 バイト157,184 バイト
関数ごとに分けた版33,552 バイト140,800 バイト
16,58416,384

Windowsの差はぴったり 16,384 バイト。16 KiB です。仕込んだ表(int × 4096)の実体そのままでした。

理由はこうです。静的リンクが取り込む単位は「関数」ではなく「オブジェクトファイル」です。1つのファイルに全部入れてしまうと、そのうち1関数でも使えばファイルごと付いてきます。呼んでいない関数も、その関数が抱えている16 KiBの表も、まとめて。

分けておけば、要るファイルだけが取り込まれます。clang でも MSVC でも、既定では同じ挙動でした

★「静的リンクは太る」は半分だけ本当
太るかどうかはライブラリの作り方で決まります。macOSでは、分割版(33,552)と動的版(33,560)が8バイト差まで縮みました。
「静的にすると大きくなる」と言われたら、そのライブラリがどう作られているかを見に行くのが正しい反応でした。

★分割できないなら、リンカに落としてもらう

ここで当然の疑問が出ます。他人が作ったライブラリは、分割し直せません。

使っていないものはリンカに捨ててもらえないのか。両OSで試しました。予想は2回とも外れました。

分割できないなら、リンカに落としてもらう 同じ「使っていないものを削る」でも、OSによって削れる粒度が違う macOS(clang / ld64) シンボル単位で追えるので指定は1つ 指定なし 50,136 落ちない -Wl,-dead_strip だけ 33,544 落ちた セクション指定を足しても 33,544 落ちた Windows(MSVC / link.exe) COMDATに分けてからでないと落とせない 既定 157,184 落ちない /Gy + /OPT:REF 157,184 落ちない /Gw + /OPT:REF 140,800 落ちた ★予想は2回とも外れた。macOSは -dead_strip だけで足り、セクション指定は1バイトも変えない。 ★Windowsは /Gy(関数)では1バイトも減らない。余計な重りが「データ」なので /Gw が要る。   実際 .text は A〜F すべて 15000 のまま動かず、減ったのは .rdata(F000→B000=16 KiB)だけだった。
分割せずにリンカへ落としてもらう場合、必要な指定がOSで違う

macOS:指定は1つだけでよかった

最初は「セクションに分ける指定(-ffunction-sections -fdata-sections)と、捨てる指定(-dead_strip)の両方が要る」と思っていました。切り分けたら違いました。

コンパイル指定リンク指定サイズ(3回とも同値)未使用関数
なしなし50,136残っている
なし-Wl,-dead_strip33,544落ちた
-ffunction-sections-Wl,-dead_strip33,544落ちた
-fdata-sections-Wl,-dead_strip33,544落ちた
両方-Wl,-dead_strip33,544落ちた

セクション指定は1バイトも変えません。macOSのリンカはシンボル単位で「どこから辿り着けるか」を追えるので、あらかじめ切り分けておく必要がないのでした。

Windows:関数を分ける指定では、1バイトも減らなかった

Windowsには /Gy(関数を独立した塊にする)と /OPT:REF(使われていない塊を捨てる)があります。これで落ちるはず、と思って測りました。

指定実行ファイル.rdata
A既定157,184F000
B/Gy だけ157,184F000
C/Gy/OPT:REF157,184F000
D関数ごとに分割(参考)140,800B000
E/Gy/Gw/OPT:REF140,800B000
F/Gw/OPT:REF/Gy なし)140,800B000

Cは1バイトも減りませんでした。

理由が分かってみると当たり前でした。削りたいものが「関数」ではなく「データ」だったからです。

証拠は .text(コードが入る場所)です。A から F まですべて 15000 のまま動いていません。減ったのは .rdata(読み取り専用データ)の F000B000、つまり 16,384 バイトだけ。重りの正体は表そのものでした。

  • /Gy が独立した塊にするのは関数だけ。データはそのままなので、/OPT:REF に捨てる対象がない
  • /Gwデータを独立した塊にする。これを足して初めて落ちた
  • ★今回は /Gy は無くてもよく、/Gw/OPT:REF だけで分割版と1バイト差なく一致しました

★同じ「使っていないものを落とす」でも、落とせる粒度が違う
Windowsのリンカは「独立して捨ててよい」と印が付いた塊しか捨てられません。だから先に /Gy(関数)と /Gw(データ)で分けておく必要があります。
macOSのリンカはシンボル単位で辿れるので、その準備が要りません。
「同じことをする指定」に見えて、前提にしている仕組みが違いました

関数を分けるデータを分ける落とす
macOS不要不要-Wl,-dead_strip
Windows/Gy/Gw/OPT:REF
Linux-ffunction-sections-fdata-sections-Wl,--gc-sections

/OPT:REF はリリースビルドでは既定で有効ですが、増分リンクが有効だと無効化されます。今回は /INCREMENTAL:NO を併記しました。

起動にかかる時間

「動的リンクは起動が遅い」とよく言われます。どのくらい遅いのか測りました。

起動にかかる時間 ★OSをまたいで絶対値は比べない(測り方が違う)。順番と比率で読む macOS(3回の平均) 静的リンク 1.517 ms 静的(分割) 1.494 ms 動的リンク 1.539 ms 実行時リンク 1.628 ms Windows(3ラウンドの中央値) 静的リンク 6.218 ms 静的(分割) 6.278 ms 動的リンク 6.659 ms 実行時リンク 6.637 ms ★どちらのOSでも「静的がいちばん速い」までは同じ。ただしどこが同着になるかが逆だった。   macOS:静的と動的が回によって入れ替わる(差1.5%)/実行時だけ明確に遅い(+7.3%)   Windows:静的が明確に速い(+7.1%)/動的と実行時が同着(差0.3%・順番が入れ替わる)   ※Windowsの絶対値にはPowerShell経由でプロセスを起動するぶんが乗っている。
起動にかかる時間(★OSをまたいで絶対値は比べない)
macOS(3回の平均)Windows(3ラウンドの中央値)
静的リンク1.517 ms6.218 ms
静的(分割)1.494 ms6.278 ms
動的リンク1.539 ms6.659 ms
実行時リンク1.628 ms6.637 ms

OSをまたいで絶対値を比べないでください。Windowsの数字にはPowerShell経由でプロセスを起動するぶんが乗っていて、測り方が揃っていません。順番と比率だけを読みます。

読めたことは2つです。

①「静的がいちばん速い」までは両OSで同じでした。ここは理屈どおりです。

②ところが、どこが同着になるかが逆でした。

  • macOS:静的と動的の差は1.5%で、回によって順番が入れ替わります(ある回では静的1.536に対し動的1.530)。明確に遅いのは実行時リンクだけ(+7.3%)
  • Windows:静的が明確に速い(+7.1%)。逆に動的と実行時が0.3%差で同着で、3ラウンドとも順番が入れ替わりました

どちらにしても数%の話です。1回起動するのに1〜7ミリ秒。「起動が遅いから静的にする」という理由づけは、この規模では弱いと言ってよさそうです。

★Windowsの .lib は2種類ある

冒頭で引っかかっていた話です。実測すると、はっきりしました。

同じ「.lib」でも、中身は別物 Windowsの拡張子がいちばん紛らわしいところ math_static.lib 静的ライブラリ 18,340 バイト ・.text$mn(コード) ・.rdata 4004(16KiBの表) ・→ 実行ファイルに写し取られる math.lib インポートライブラリ 2,168 バイト ・.idata$2 〜 .idata$6 だけ ・★.text が1バイトも無い ・→ DLLの入口の名簿でしかない ★サイズは 8.5 倍違う。拡張子が同じなので、置き換えても気づきにくい。
Windowsの .lib は2種類あり、中身がまったく違う
ファイル種類サイズ中身
math_static.lib静的ライブラリ18,340.text$mn(コード)+ .rdata 4004(例の16 KiBの表)
math.libインポートライブラリ2,168.idata$2.idata$6 だけ

8.5倍の差があります。そして決定的なのは、インポートライブラリのほうに.text が1バイトも無いことです。

Dump of file out\math.lib
File Type: LIBRARY

     Exports
       ordinal    name
                  ml_add
                  ml_fib
                  ml_unused_big
                  ml_version

  Summary
          BA .debug$S
          14 .idata$2
          14 .idata$3
           8 .idata$4
           8 .idata$5
           A .idata$6

名前と番号の対応表しか入っていません。コードはDLLの側にあります。この .lib は「DLLの入口の名簿」であって、ライブラリの本体ではないのでした。

だから .lib を渡されたとき、それだけで動くのか、DLLも一緒に要るのかは、拡張子では分かりません。サイズを見るか、中を覗くしかない。ここはWindowsのつまずきどころだと思います。

★★Windowsだけ、ヘッダが3つの場合を知っている必要がある

ここはeightが間違えた話です。

macOSとLinuxでは、static を付けない限り関数は外から見えます。ところがWindowsは「これは外に出す」と明示しないと出ません。そこで、こういう印を用意しました。

#if defined(_WIN32)
#  if defined(MATHLIB_BUILD)
#    define MLAPI __declspec(dllexport)   // DLLを作る側
#  else
#    define MLAPI __declspec(dllimport)   // DLLを使う側
#  endif
#else
#  define MLAPI
#endif

これで足りると思っていました。Windowsで組み立てたら、静的リンクが一度も通りませんでした

main_s.obj : error LNK2019: 未解決の外部シンボル __imp_ml_add が関数 main で参照されました
out\app_static.exe : fatal error LNK1120: 3 件の未解決の外部参照

抜けていたのは「静的ライブラリを使う側」の場合でした。上の書き方だと、DLLを作るとき以外は必ず dllimport になります。すると main.c__imp_ml_add(DLL経由の入口)を探しに行くのに、静的ライブラリに入っているのは素の ml_add なので、噛み合いません。

#if defined(_WIN32)
#  if defined(MATHLIB_STATIC)
#    define MLAPI                          // ★静的リンクで使う(これが抜けていた)
#  elif defined(MATHLIB_BUILD)
#    define MLAPI __declspec(dllexport)    // DLLを作る
#  else
#    define MLAPI __declspec(dllimport)    // DLLを使う
#  endif
#else
#  define MLAPI                            // macOS/Linux は全部これでよい
#endif

同じヘッダが3つの場合を知っていなければならない、というのがWindowsの姿でした。macOSとLinuxは1つも要りません

もうひとつ外れた予想があります。「印を付けなければ何も出ないはず」と思って試したら、全部エクスポートされていました

math_noexport.dll        math.dll
  1 ml_add                 1 ml_add
  2 ml_fib                 2 ml_fib
  3 ml_unused_big          3 ml_unused_big
  4 ml_version             4 ml_version

ライブラリ側のソースも同じヘッダを読むので、印が dllimport に化けていました。つまり「持ち込むと宣言した関数を、その場で定義している」状態です。コンパイラはこう言いました。

warning C4273: 'ml_add': dll リンケージが一貫していません。

そして定義している側を優先して、結局エクスポート扱いにしていました。警告を読み飛ばすと、意図と逆のものができあがります。

本当に何も付けない版で作り直すと、期待どおりでした。

[note] NO import library - nothing exported
Dump of file math_truenoexport.dll
File Type: DLL
  Summary          ← エクスポートの節が丸ごと無い

そのDLLを使おうとすると、こうなります。

LINK : fatal error LNK1181: 入力ファイル 'math_truenoexport.lib' を開けません。

「関数が見つからない」と怒られるのではなく、そもそも名簿ファイルが作られない。これがWindowsの答えでした。エラーメッセージだけ見ると原因が分かりにくいところです。

★「見つからない」ときの振る舞いがOSで違う

配布でいちばん転ぶのがここだと思います。動的ライブラリを消して、それぞれ実行しました。

macOS は、探しに行った場所を全部並べてくれます。

dyld[35485]: Library not loaded: @rpath/libmath.dylib
  Referenced from: .../out/app_dynamic
  Reason: tried: '.../out/libmath.dylib' (no such file),
                 '.../out/libmath.dylib' (no such file)

Windows は、リンクの仕方で出るものが変わりました。

アプリmath.dll を消したとき
動的リンク(起動時)終了コード 0xC0000135 だけ。標準出力にも標準エラーにも何も出ない
実行時リンクLoadLibrary 失敗: error 126(自分のコードで書いたメッセージ)
静的リンク平然と動く(そもそもDLLを見ていない)

動的リンクのほうは、プログラムが起動する前にOSのローダが落としているので、こちらのコードは1行も走りません。だからメッセージを出しようがないのでした。

★Windowsは、原因を教えてくれない
「配ったアプリが、相手の環境で何も言わずに起動しない」の正体は、たいていこれです。
自分でメッセージを出したいなら、実行時リンクにするしかありません。起動時リンクを選んだ時点で、この診断は手放しています。

結局どれを選ぶか

こういうとき選ぶもの理由
配布物を1個で済ませたい静的リンク持っていくファイルが実行ファイルだけになる
配ったあとに直したい動的リンク★ライブラリだけ差し替えれば追従する
OSの機能を使う動的リンク(選べない)OS側が動的ライブラリで提供している
後から機能を足せるようにしたい実行時リンクプラグインの仕組みそのもの
読み込み失敗を自分で扱いたい実行時リンク★起動時リンクだと何も言わずに落ちる(Windows)
静的にしたらサイズが増えた★ライブラリの作り方を疑う。指定で落とせることも多い

ひとつ言えるのは、起動時間を理由に選ぶ場面はあまり無さそうだということでした。差は数%で、しかもOSによってどこに出るかが変わります。

まとめ

  • ★★拡張子は何も決めていない。macOSで作った .dylib.so に改名しても読めるが、本物のWindows DLLは .dylib に改名しても読めない(slice is not valid mach-o file)。決めているのは中身の形式(Mach-O / ELF / PE-COFF)
  • 覚える軸は「いつ名前が解決されるか」の1つだけ。ビルド時(静的)・起動時(動的)・実行中(実行時)で、拡張子の違いはそこから派生している
  • ライブラリだけ作り直すと、静的リンクだけが追従しない。1.0のまま変わらない様子がいちばん分かりやすい
  • ★★静的リンクは「関数」ではなく「オブジェクトファイル」単位で取り込む。呼んでいない関数が16 KiBの表ごと付いてきた(Windowsの差はぴったり16,384バイト)
  • 「静的リンクは太る」はライブラリの作り方次第。分割版と動的版がmacOSで8バイト差まで縮んだ
  • ★★分割できなくてもリンカに落としてもらえる。ただし前提が違う。macOSは -dead_strip だけでよく、セクション指定は1バイトも変えない。Windowsは /Gy(関数)だけでは1バイトも減らず/Gw(データ)が要った
  • 今回の重りは丸ごとデータだった.text はA〜Fすべて動かず、減ったのは .rdata だけ
  • 起動時間の差は数%。しかもどこが同着になるかがOSで逆(macOSは静的と動的、Windowsは動的と実行時)
  • Windowsの .lib は2種類。静的ライブラリ18,340バイトに対しインポートライブラリは2,168バイトで、.text が1バイトも無い
  • ★★Windowsのヘッダは3つの場合を知っている必要がある(DLLを作る/DLLを使う/静的に使う)。macOSとLinuxは1つも要らない。ここを1つ落として LNK2019 になった
  • 印を付けなければ出ない、とは限らないwarning C4273 を出したうえで、定義側を優先してエクスポートされていた
  • DLLが無いとき、Windowsの起動時リンクは何も言わない(終了コード 0xC0000135 のみ)。macOSは探した場所を全部出す

調べる前は「拡張子がOSごとに違うだけだろう」と思っていました。測ってみると、同じ考え方で名前が違うだけのものと、仕組みから違うものがはっきり分かれていました。

前者が「静的と動的」、後者が「エクスポートの明示」と「落とせる粒度」です。前者だけ知っていれば大体足りますが、後者を知らないとWindowsで詰まります。今回eightが LNK2019 で止まったのが、まさにそれでした😅

そして予想が3回外れました。ヘッダの分岐・/Gy・macOSのセクション指定。3つとも「そういうものだろう」と思い込んでいた部分で、測らなければ気づかないままでした。思い込みの数だけ実験を用意しておくと、記事が勝手に面白くなるというのが今回の収穫です😊

使った材料は、Cのソース4本とビルドスクリプトだけです。手元のライブラリに差し替えれば、同じ表が作れると思います。

それでは、今回はここまで。最後までありがとうございました😊